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