#include #include #include // Memori Permanen #include // Portal Config #include // Format Data #include // Kedipan LED Status #include #include #include // ========================================== // PIN DEFINITIONS (LoRa SPI & Control) // ========================================== #define LORA_SS D8 // NSS/CS (Tetap di bawah) #define LORA_RST D0 // Reset LoRa (Paling Atas) #define LORA_DIO0 D1 // Interrupt LoRa (Urutan ke-2) #define BUZZER_PIN D2 // Buzzer (Urutan ke-3) #define BUTTON_PIN D3 // Pin Reset Button (Urutan ke-4) #define STATUS_LED_PIN D4 // Pin LED Status (Urutan ke-5) // ========================================== // VARIABLES // ========================================== char n8n_url[200] = ""; bool shouldSaveConfig = false; typedef struct struct_message { char text[220]; // Kapasitas dikurangi untuk 4 variabel sinyal int msgType; // 0=Request, 1=Response, 2=Ping, 3=Pong int packetId; int totalPackets; int senderId; // 1=Node1, 2=Node2, 3=Repeater int rssi_gw; // Sinyal Hop 1 (A -> GW) float snr_gw; int rssi_b; // Sinyal Hop 2 (GW -> B) float snr_b; } struct_message; struct_message recvData; struct_message sendData; char jsonBufferReq[1024]; volatile bool dataReceived = false; volatile bool isPingRequested = false; wl_status_t lastWifiStatus = WL_IDLE_STATUS; unsigned long lastWifiCheck = 0; unsigned long lastDisconnectBeep = 0; // Reset Button Helpers unsigned long pressStartTime = 0; bool isPressed = false; // Status LED Objects Ticker statusTicker; // ========================================== // HELPER FUNCTIONS // ========================================== // Buzzer Beep Function void beep(int count, int durationMs) { for(int i=0; i buf(new char[size]); configFile.readBytes(buf.get(), size); DynamicJsonDocument json(1024); deserializeJson(json, buf.get()); strcpy(n8n_url, json["n8n_url"]); Serial.print("Config Loaded: "); Serial.println(n8n_url); } } } else { Serial.println("Gagal mematikan LittleFS"); } } // Save Config ke LittleFS void saveConfig() { Serial.println("Menyimpan Config ke LittleFS..."); DynamicJsonDocument json(1024); json["n8n_url"] = n8n_url; File configFile = LittleFS.open("/config.json", "w"); if (!configFile) { Serial.println("Gagal membuka file config untuk menulis"); } serializeJson(json, configFile); configFile.close(); } // Send Data via LoRa void sendDataLoRa(struct_message *data) { LoRa.beginPacket(); LoRa.write((uint8_t*)data, sizeof(struct_message)); LoRa.endPacket(); } // ========================================== // SETUP // ========================================== void setup() { Serial.begin(115200); pinMode(BUZZER_PIN, OUTPUT); pinMode(BUTTON_PIN, INPUT_PULLUP); pinMode(STATUS_LED_PIN, OUTPUT); digitalWrite(BUZZER_PIN, LOW); digitalWrite(STATUS_LED_PIN, LOW); // Mulai kedipan lambat (sedang mencari WiFi) statusTicker.attach(0.6, tick); // 1. Baca Konfigurasi Tersimpan loadConfig(); // 2. Setup WiFiManager WiFiManager wm; wm.setSaveConfigCallback(saveConfigCallback); wm.setAPCallback(configModeCallback); // Tambahkan Input Custom untuk URL n8n WiFiManagerParameter custom_n8n_url("n8n", "N8N Webhook URL", n8n_url, 199); wm.addParameter(&custom_n8n_url); // Suara tanda portal siap beep(1, 300); // Start Portal (Hotspot: Stunting-Gateway-Config) if (!wm.autoConnect("Stunting-Gateway-Config")) { Serial.println("Gagal konek WiFi."); delay(3000); ESP.restart(); } // Ambil nilai dari portal strcpy(n8n_url, custom_n8n_url.getValue()); // Matikan Ticker setelah konek statusTicker.detach(); // Simpan jika user mengubah nilainya if (shouldSaveConfig) { saveConfig(); beep(2, 100); } Serial.println("Connected to WiFi!"); digitalWrite(STATUS_LED_PIN, HIGH); // LED Nyala Diam (Sukses) Serial.print("IP: "); Serial.println(WiFi.localIP()); Serial.print("N8N URL: "); Serial.println(n8n_url); beep(1, 100); // Setup LoRa LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0); if (!LoRa.begin(433E6)) { Serial.println("Error initializing LoRa"); while (1) { delay(1000); } } Serial.println("Gateway LoRa Ready (V2 Piggybacking)"); lastWifiStatus = WiFi.status(); } // ========================================== // LOOP // ========================================== void loop() { // --- 1. RESET BUTTON MONITORING (D4) --- if (digitalRead(BUTTON_PIN) == LOW) { if (!isPressed) { isPressed = true; pressStartTime = millis(); } unsigned long pressDuration = millis() - pressStartTime; if (pressDuration > 2000) { digitalWrite(BUZZER_PIN, (millis() / 200) % 2); } // Bunyi alarm pelan if (pressDuration >= 5000) { Serial.println("!!!!!!! FACTORY RESET TRIGGERED !!!!!!!"); beep(1, 2000); // Beep panjang tanda sukses reset WiFiManager wm; wm.resetSettings(); // Hapus WiFi LittleFS.format(); // Hapus Config n8n ESP.restart(); } } else { if (isPressed) { isPressed = false; digitalWrite(BUZZER_PIN, LOW); } } // --- 2. WIFI STATUS MONITORING --- wl_status_t currentStatus = WiFi.status(); if (currentStatus != lastWifiStatus) { if (currentStatus == WL_CONNECTED) { Serial.println("WiFi Reconnected!"); digitalWrite(STATUS_LED_PIN, HIGH); beep(2, 100); } else { Serial.println("WiFi Disconnected"); } lastWifiStatus = currentStatus; } // Alarm WiFi Offline (Setiap 10s) if (currentStatus != WL_CONNECTED) { digitalWrite(STATUS_LED_PIN, LOW); // Lampu Mati jika Offline if (millis() - lastDisconnectBeep > 10000) { lastDisconnectBeep = millis(); beep(1, 400); Serial.println("Status: Offline..."); } } // --- 3. LORA RECEIVE LOGIC --- int packetSize = LoRa.parsePacket(); if (packetSize == sizeof(struct_message)) { uint8_t buffer[sizeof(struct_message)]; int i = 0; while (LoRa.available() && i < sizeof(struct_message)) { buffer[i] = LoRa.read(); i++; } memcpy(&recvData, buffer, sizeof(struct_message)); recvData.text[219] = '\0'; // ensure null termination (kapasitas diubah ke 220) // STRICT ROUTING: HANYA terima data dari Node Perantara (Repeater / ID 3) // Abaikan jika data datang langsung dari Node 1 atau pantulan Node 2 sendiri if (recvData.senderId != 3) return; if (recvData.msgType == 0) { // DATA REQUEST if (recvData.packetId == 0) { memset(jsonBufferReq, 0, sizeof(jsonBufferReq)); } int currentLen = strlen(jsonBufferReq); int spaceLeft = sizeof(jsonBufferReq) - currentLen - 1; if (spaceLeft > 0) { strncat(jsonBufferReq, recvData.text, spaceLeft); } if (recvData.packetId == recvData.totalPackets - 1) { // Menangkap RSSI dan SNR Hop 2 di Node B ini recvData.rssi_b = LoRa.packetRssi(); recvData.snr_b = LoRa.packetSnr(); dataReceived = true; beep(1, 50); } } else if (recvData.msgType == 2) { isPingRequested = true; beep(1, 40); } } // --- 4. HANDLE PING (UI CHECK) --- if (isPingRequested) { isPingRequested = false; sendData.msgType = 3; sendData.packetId = 0; sendData.totalPackets = 1; sendData.senderId = 2; if (WiFi.status() != WL_CONNECTED) { strcpy(sendData.text, "WIFI_ERROR"); } else if (strlen(n8n_url) < 10) { strcpy(sendData.text, "N8N_ERROR"); // Belum diatur } else { // Cukup pastikan WiFi terhubung dan URL N8N Tersetting. // JANGAN lakukan POST ke webhook n8n agar tidak mengirim eksekusi palsu (ghost trigger) strcpy(sendData.text, "READY"); } sendDataLoRa(&sendData); } // --- 5. HANDLE DATA (SEND TO n8n) --- if (dataReceived) { dataReceived = false; Serial.println("Forwarding to n8n..."); // Inject RSSI dan SNR ke dalam JSON Payload sebelum dikirim String payloadReq = String(jsonBufferReq); int lastBrace = payloadReq.lastIndexOf('}'); if (lastBrace != -1) { payloadReq.remove(lastBrace); payloadReq += ",\"rssi_b\":" + String(recvData.rssi_b) + ",\"snr_b\":" + String(recvData.snr_b) + "}"; } if (WiFi.status() == WL_CONNECTED && strlen(n8n_url) > 10) { WiFiClientSecure client; client.setInsecure(); HTTPClient http; if (http.begin(client, n8n_url)) { http.setTimeout(30000); http.addHeader("Content-Type", "application/json"); int httpCode = -1; int retryCount = 0; while (retryCount < 3) { httpCode = http.POST(payloadReq); // Kirim Payload yang sudah ada titipan RSSI if (httpCode > 0) break; retryCount++; delay(1500); } String payload = "{}"; if (httpCode > 0) { payload = http.getString(); Serial.print("n8n RESPONSE: "); Serial.println(payload); beep(2, 100); } else { payload = "{\"status\":\"ERROR\",\"status_gizi\":\"error\",\"ai_narrative\":\"Gagal koneksi n8n.\"}"; beep(1, 1000); } http.end(); // CHUNKING LORA int payloadLen = payload.length(); int chunkSize = 215; // Disesuaikan dengan kapasitas struct_message baru int totalPackets = (payloadLen + chunkSize - 1) / chunkSize; for(int i=0; i