TKK_E32231517/Node A - ESP32/node1_lora++_v2.ino

438 lines
14 KiB
C++

#include <SPI.h>
#include <LoRa.h>
#include <ArduinoJson.h>
#include "HX711.h"
// ==========================================
// ==========================================
// PIN DEFINITIONS (LoRa SPI - Custom Layout)
// ==========================================
#define LORA_SCK 22 // D22
#define LORA_MISO 21 // D21
#define LORA_MOSI 19 // D19
#define LORA_SS 18 // D18
#define LORA_RST 23 // D23
#define LORA_DIO0 34 // D34 (Sisi Kiri, Input-Only, Sangat Aman)
// ==========================================
// PIN DEFINITIONS (SENSOR & LED - SISI KANAN ESP32)
// ==========================================
// SENSOR (Dipindah ke KIRI karena D15 & D2 adalah Strapping Pin)
#define TRIG_PIN 25 // D25
#define ECHO_PIN 26 // D26
// LED & BUZZER (SISI KANAN - Aman)
#define LED_BLUE 17 // TX2 (D17)
#define LED_GREEN 16 // RX2 (D16)
#define LED_RED 4 // D4
#define BUZZER_PIN 15 // D15 (Buzzer untuk indikator LoRa)
// ==========================================
// PIN DEFINITIONS (LOADCELL - SISI KIRI ESP32)
// ==========================================
// Dipindah ke D32 & D33 karena D18 & D19 sekarang dipakai LoRa
#define HX711_DT 32 // D32
#define HX711_SCK 33 // D33
// ==========================================
// SETTINGS
// ==========================================
#define MIN_DIST 2.0
#define MAX_DIST 200.0
#define TIMEOUT_MS 3000
// ==========================================
// KALIBRASI HC-SR04
// ==========================================
#define TINGGI_ALAT 79.27 // Tinggi sensor dari lantai ke alas (cm) - diukur manual
float SENSOR_OFFSET = -4.94; // Offset koreksi hardware sensor (dari kalibrasi)
float TINGGI_OFFSET = 0.0; // Offset koreksi tambahan jika ada selisih pembacaan
// ==========================================
// KALIBRASI LOAD CELL
// ==========================================
float calibration_factor = -103.8; // Dari hasil kalibrasi (loadcell_ultrasonik_kalibrasi.ino)
float weightThreshold = 2.0; // Ambang batas dianggap kosong (gram)
unsigned long emptyStartTime = 0;
bool isEmptyScale = false;
// ==========================================
// VARIABLES
// ==========================================
bool isConnected = false;
bool isBalitaSelected = false;
unsigned long lastHeartbeatTime = 0;
bool isLedLocked = false;
char jsonBuffer[800];
// Measuring Variables
float lastDistance = 0;
unsigned long lastChangeTime = 0;
unsigned long lastScaleRead = 0;
// Worker Modes
enum WorkMode { MODE_MEASURE, MODE_ANALYZE };
WorkMode currentMode = MODE_MEASURE;
// Objects
HX711 scale;
// Packet Structure
typedef struct struct_message {
char text[220]; // Dikurangi untuk memberi ruang pada data 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 myData;
struct_message recvData;
// ==========================================
// HELPER FUNCTIONS
// ==========================================
void setStatus(int r, int g, int b) {
digitalWrite(LED_RED, r);
digitalWrite(LED_GREEN, g);
digitalWrite(LED_BLUE, b);
}
// Send Data via LoRa
bool sendDataLoRa(struct_message *data) {
// Bunyikan Buzzer saat mengirim data
digitalWrite(BUZZER_PIN, HIGH);
delay(50);
digitalWrite(BUZZER_PIN, LOW);
LoRa.beginPacket();
LoRa.write((uint8_t*)data, sizeof(struct_message));
LoRa.endPacket();
return true;
}
// ==========================================
// SETUP
// ==========================================
void setup() {
Serial.begin(115200);
// Sensor & LED Pins
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT_PULLDOWN); // Gunakan PULLDOWN agar tidak menangkap noise saat sensor tidak terhubung
pinMode(LED_RED, OUTPUT);
pinMode(LED_GREEN, OUTPUT);
pinMode(LED_BLUE, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
// Matikan semua LED di awal
setStatus(0, 0, 0);
// 1. Setup Loadcell (HX711)
scale.begin(HX711_DT, HX711_SCK);
scale.set_scale(calibration_factor);
// Wajib: Beri Pull-Up pada pin DT agar jika timbangan tidak dicolok, dia akan diam di HIGH (Not Ready).
// Jika tidak di-pullup, pin yang mengambang akan dianggap LOW (Ready) oleh ESP32.
pinMode(HX711_DT, INPUT_PULLUP);
// Beri waktu maksimal 3 detik untuk modul HX711 menyala dan siap (sama seperti kalibrasi)
unsigned long startWait = millis();
while (!scale.is_ready() && millis() - startWait < 3000) {
delay(10);
}
// Cek apakah sensor timbangan benar-benar terhubung
if (scale.is_ready()) {
scale.tare(); // Zero the scale on startup
digitalWrite(LED_GREEN, HIGH); // Hijau = HX711 Ready
} else {
Serial.println("Warning: HX711 Loadcell not detected or not ready yet!");
}
// 2. Setup SPI & LoRa
SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS);
LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0);
if (!LoRa.begin(433E6)) {
Serial.println("Error initializing LoRa");
} else {
digitalWrite(LED_RED, HIGH); // Merah = LoRa Ready
}
delay(500); // Jeda tambahan agar sensor ultrasonik stabil sebelum dites
// 3. Cek Ultrasonik (Kirim 1x Ping)
digitalWrite(TRIG_PIN, LOW); delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH); delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
long initDuration = pulseIn(ECHO_PIN, HIGH, 30000);
if (initDuration > 0) digitalWrite(LED_BLUE, HIGH); // Biru = Ultrasonik Ready
Serial.println("LoRa Node A (ESP32-Integrated-Pro) Ready (V2 Piggybacking)");
}
// ==========================================
// LOOP
// ==========================================
void loop() {
// 1. CEK DATA DARI WEB APP (SERIAL) --------------------
if (Serial.available() > 0) {
String cmd = Serial.readStringUntil('\n');
cmd.trim();
if (cmd == "CMD:CONNECT") {
isConnected = true;
isBalitaSelected = false;
lastHeartbeatTime = millis();
isLedLocked = false;
// Buzzer Beep Cepat saat Terkoneksi ke Web
for(int i=0; i<3; i++) {
digitalWrite(BUZZER_PIN, HIGH); delay(100);
digitalWrite(BUZZER_PIN, LOW); delay(100);
}
} else if (cmd == "CMD:PING") {
if (!isConnected) isConnected = true;
lastHeartbeatTime = millis();
} else if (cmd == "CMD:DISCONNECT") {
isConnected = false;
isBalitaSelected = false;
isLedLocked = false;
} else if (cmd == "CMD:TARE") {
scale.tare();
Serial.println("TARE:OK");
} else if (cmd == "CMD:LOCK_LED") {
isLedLocked = true;
} else if (cmd == "CMD:UNLOCK_LED") {
isLedLocked = false;
} else if (cmd == "CMD:SET_MODE:MEASURE") {
currentMode = MODE_MEASURE;
isLedLocked = false;
Serial.println("MODE:MEASURE:OK");
} else if (cmd == "CMD:SET_MODE:ANALYZE") {
currentMode = MODE_ANALYZE;
isLedLocked = true;
Serial.println("MODE:ANALYZE:OK");
} else if (cmd == "CMD:BALITA_SELECTED") {
isBalitaSelected = true;
Serial.println("BALITA:SELECTED:OK");
} else if (cmd == "CMD:BALITA_UNSELECTED") {
isBalitaSelected = false;
Serial.println("BALITA:UNSELECTED:OK");
} else if (cmd == "CMD:PING_LOCAL") {
// 1. Cek Sensor Ultrasonik Lokal
digitalWrite(TRIG_PIN, LOW); delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH); delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
long duration = pulseIn(ECHO_PIN, HIGH, 30000);
float distance = duration * 0.0343 / 2;
// 2. Cek Loadcell Lokal
bool loadcellReady = scale.is_ready();
String resp = "PONG:A:READY";
if (distance > MIN_DIST && distance < MAX_DIST) resp += " PONG:SENSOR:DETECTED";
else resp += " PONG:SENSOR:EMPTY";
if (loadcellReady) resp += " PONG:LOADCELL:READY";
else resp += " PONG:LOADCELL:ERROR";
Serial.println(resp);
} else if (cmd == "CMD:PING_LORA") {
// Konfirmasi ke Web App bahwa Node A (Local) juga Ready
Serial.println("PONG:A:READY");
// Ping Node B via LoRa
myData.msgType = 2; // PING
myData.packetId = 0;
myData.totalPackets = 1;
myData.senderId = 1; // Node 1
strcpy(myData.text, "PING");
sendDataLoRa(&myData);
} else if (cmd.startsWith("CMD:SEND_DATA:")) {
String json = cmd.substring(14);
int totalLength = json.length();
int chunkSize = 215; // Dikurangi agar muat di text[220]
int totalPackets = (totalLength + chunkSize - 1) / chunkSize;
for (int i = 0; i < totalPackets; i++) {
memset(&myData, 0, sizeof(struct_message));
myData.msgType = 0; // REQUEST
myData.packetId = i;
myData.totalPackets = totalPackets;
myData.senderId = 1; // Node 1
String chunk = json.substring(i * chunkSize, min((i + 1) * chunkSize, totalLength));
strncpy(myData.text, chunk.c_str(), sizeof(myData.text) - 1);
myData.text[sizeof(myData.text) - 1] = '\0';
sendDataLoRa(&myData);
if (i < totalPackets - 1) {
delay(400); // Beri jeda 400ms agar Repeater sempat memantulkan chunk sebelumnya
}
}
}
}
// 2. CEK DATA MASUK DARI LORA (NODE B) ------------------
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';
// STRICT ROUTING: HANYA terima data dari Node Perantara (Repeater / ID 3)
// Abaikan jika data datang langsung dari Node 2 atau pantulan Node 1 sendiri
if (recvData.senderId != 3) return;
// Abaikan PING yang dipantulkan kembali oleh Repeater (agar Node A tidak beep dua kali)
if (recvData.msgType == 2) return;
// Abaikan REQUEST (Data Anda sendiri) yang dipantulkan kembali oleh Repeater
if (recvData.msgType == 0) return;
// Bunyikan Buzzer saat menerima data
digitalWrite(BUZZER_PIN, HIGH);
delay(50);
digitalWrite(BUZZER_PIN, LOW);
if (recvData.msgType == 1) { // RESPONSE (AI)
if (recvData.packetId == 0) memset(jsonBuffer, 0, sizeof(jsonBuffer));
int currentLen = strlen(jsonBuffer);
int spaceLeft = sizeof(jsonBuffer) - currentLen - 1;
if (spaceLeft > 0) strncat(jsonBuffer, recvData.text, spaceLeft);
if (recvData.packetId == recvData.totalPackets - 1) {
// Tangkap Downlink RSSI dan SNR
int rssi_a = LoRa.packetRssi();
float snr_a = LoRa.packetSnr();
String payload = String(jsonBuffer);
int lastBrace = payload.lastIndexOf('}');
if (lastBrace != -1) {
payload.remove(lastBrace);
payload += ",\"rssi_a\":" + String(rssi_a) +
",\"snr_a\":" + String(snr_a) +
",\"rssi_gw\":" + String(recvData.rssi_gw) +
",\"snr_gw\":" + String(recvData.snr_gw) + "}";
}
Serial.print("AI_RESPONSE:");
Serial.println(payload);
isLedLocked = true;
}
} else if (recvData.msgType == 3) { // PONG
String status = String(recvData.text);
if (status.indexOf("WIFI_ERROR") != -1) Serial.println("PONG:B:WIFI_ERROR");
else if (status.indexOf("N8N_ERROR") != -1) Serial.println("PONG:B:N8N_ERROR");
// Cek REPEATER_READY DULU sebelum READY, agar tidak salah tangkap (karena kata READY ada di dalamnya)
else if (status.indexOf("REPEATER_READY") != -1) Serial.println("PONG:REPEATER:READY");
else if (status.indexOf("READY") != -1) Serial.println("PONG:B:READY");
}
}
// 3. LOGIKA BUZZER DISCONNECTED ---------------------
static unsigned long lastBuzzerLoop = 0;
if (!isConnected) {
// Buzzer beep 2x cepat (Setiap 20 detik)
if (millis() - lastBuzzerLoop > 20000) {
for (int i = 0; i < 2; i++) {
digitalWrite(BUZZER_PIN, HIGH);
delay(50); // Beep singkat
digitalWrite(BUZZER_PIN, LOW);
delay(50); // Jeda antar beep
}
lastBuzzerLoop = millis();
}
} else {
// Dipastikan mati jika sudah connected
}
// 4. LOGIKA SENSOR (TINGGI & BERAT) ---------------------
// --- MODE UKUR AKTIF ---
if (!isLedLocked && isConnected && currentMode == MODE_MEASURE && isBalitaSelected) {
if (millis() - lastScaleRead > 250) {
lastScaleRead = millis();
// 4a. Baca Tinggi (Ultrasonik)
digitalWrite(TRIG_PIN, LOW); delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH); delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
long duration = pulseIn(ECHO_PIN, HIGH, 30000);
// Konversi ke cm + koreksi offset hardware sensor (dari kalibrasi)
float jarak_sensor = (duration * 0.0343 / 2) + SENSOR_OFFSET;
if (duration > 0 && jarak_sensor > MIN_DIST && jarak_sensor < MAX_DIST) {
// Rumus: Tinggi Badan = Tinggi Alat - Jarak Sensor + Offset Koreksi
float tinggi_badan = TINGGI_ALAT - jarak_sensor + TINGGI_OFFSET;
if (tinggi_badan < 0) tinggi_badan = 0;
Serial.print("TINGGI:");
Serial.println(tinggi_badan);
lastDistance = tinggi_badan;
}
// 4b. Baca Berat (HX711) - Cek langsung hardware setiap saat
if (scale.is_ready()) {
float weight_grams = scale.get_units(20); // Rata-rata 20 bacaan (lebih akurat, sesuai kalibrasi)
// === LOGIKA AUTO-TARE (DARI KALIBRASI) ===
if (abs(weight_grams) < weightThreshold) {
if (!isEmptyScale) {
emptyStartTime = millis();
isEmptyScale = true;
}
// Jika kosong selama lebih dari 2 detik -> Tare otomatis
if (millis() - emptyStartTime > 2000) {
scale.tare();
weight_grams = 0;
}
} else {
isEmptyScale = false;
}
// Konversi ke KG sesuai file kalibrasi
float weight_kg = weight_grams / 1000.0;
if (weight_kg < 0) weight_kg = 0; // Prevent negative display
Serial.print("BERAT:");
Serial.println(weight_kg, 2); // Kirim 2 angka di belakang koma
}
}
}
// 4. TIMEOUT --------------------------------------------
if (isConnected && (millis() - lastHeartbeatTime > TIMEOUT_MS)) {
isConnected = false;
isLedLocked = false;
}
delay(30);
}