program untuk node b - esp8266
This commit is contained in:
parent
fc095720be
commit
f2748ca410
|
|
@ -0,0 +1,378 @@
|
|||
#include <SPI.h>
|
||||
#include <LoRa.h>
|
||||
#include <LittleFS.h> // Memori Permanen
|
||||
#include <WiFiManager.h> // Portal Config
|
||||
#include <ArduinoJson.h> // Format Data
|
||||
#include <Ticker.h> // Kedipan LED Status
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <ESP8266HTTPClient.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
|
||||
// ==========================================
|
||||
// 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<count; i++) {
|
||||
digitalWrite(BUZZER_PIN, HIGH);
|
||||
delay(durationMs);
|
||||
digitalWrite(BUZZER_PIN, LOW);
|
||||
if (i < count-1) delay(100);
|
||||
}
|
||||
}
|
||||
|
||||
// Callback Ticker untuk berkedip
|
||||
void tick() {
|
||||
int state = digitalRead(STATUS_LED_PIN);
|
||||
digitalWrite(STATUS_LED_PIN, !state);
|
||||
}
|
||||
|
||||
// Callback WiFiManager saat masuk mode portal
|
||||
void configModeCallback (WiFiManager *myWiFiManager) {
|
||||
Serial.println("Masuk Mode Konfigurasi...");
|
||||
Serial.println(WiFi.softAPIP());
|
||||
statusTicker.attach(0.1, tick); // Kedip super cepat saat Portal Aktif
|
||||
}
|
||||
|
||||
// Callback Simpan Konfigurasi
|
||||
void saveConfigCallback () {
|
||||
Serial.println("Menerima Konfigurasi Baru...");
|
||||
shouldSaveConfig = true;
|
||||
}
|
||||
|
||||
// Load Config dari LittleFS
|
||||
void loadConfig() {
|
||||
if (LittleFS.begin()) {
|
||||
if (LittleFS.exists("/config.json")) {
|
||||
File configFile = LittleFS.open("/config.json", "r");
|
||||
if (configFile) {
|
||||
size_t size = configFile.size();
|
||||
std::unique_ptr<char[]> 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<totalPackets; i++) {
|
||||
memset(&sendData, 0, sizeof(struct_message));
|
||||
sendData.msgType = 1; // RESPONSE
|
||||
sendData.packetId = i;
|
||||
sendData.totalPackets = totalPackets;
|
||||
sendData.senderId = 2;
|
||||
int startIdx = i * chunkSize;
|
||||
String chunk = payload.substring(startIdx, startIdx + chunkSize);
|
||||
strncpy(sendData.text, chunk.c_str(), 219);
|
||||
sendDataLoRa(&sendData);
|
||||
|
||||
if (i < totalPackets - 1) {
|
||||
delay(400); // Beri jeda 400ms agar Repeater sempat memantulkan chunk sebelumnya
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
beep(3, 200);
|
||||
String err = "{\"status_gizi\":\"offline\",\"ai_narrative\":\"Gateway Belum Siap.\"}";
|
||||
memset(&sendData, 0, sizeof(struct_message));
|
||||
sendData.msgType = 1;
|
||||
sendData.packetId = 0;
|
||||
sendData.totalPackets = 1;
|
||||
sendData.senderId = 2; // Node 2
|
||||
strncpy(sendData.text, err.c_str(), 219);
|
||||
sendDataLoRa(&sendData);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue