fix: menu hamburger
This commit is contained in:
parent
8228484cfa
commit
3577888040
|
|
@ -1,375 +0,0 @@
|
|||
/**
|
||||
* ============================================
|
||||
* Smart Rack Security System - ESP32
|
||||
* PIR + Reed Switch + SW420 + LoRa + Laravel API
|
||||
* ============================================
|
||||
*/
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <SPI.h>
|
||||
#include <LoRa.h>
|
||||
|
||||
// ============================================
|
||||
// WIFI
|
||||
// ============================================
|
||||
const char* ssid = "gege";
|
||||
const char* password = "biasaaja";
|
||||
|
||||
// GANTI DENGAN IP LARAVEL KAMU
|
||||
const char* API_BASE = "https://keamanan-rak-barang-production.up.railway.app/api";
|
||||
|
||||
// ============================================
|
||||
// DEVICE
|
||||
// ============================================
|
||||
const int DEVICE_ID = 1;
|
||||
const String NODE_ID = "LORA_001";
|
||||
|
||||
// ============================================
|
||||
// PIN SENSOR
|
||||
// ============================================
|
||||
#define PIR_PIN 33
|
||||
#define REED_PIN 32
|
||||
#define SW420_PIN 34
|
||||
#define BUZZER 25
|
||||
#define LED_MERAH 13
|
||||
#define LED_HIJAU 4
|
||||
|
||||
// ============================================
|
||||
// PIN LORA SX1278 RA-02
|
||||
// ============================================
|
||||
#define LORA_SS 27
|
||||
#define LORA_RST 14
|
||||
#define LORA_DIO0 26
|
||||
|
||||
// ============================================
|
||||
// VARIABEL
|
||||
// ============================================
|
||||
int pirState;
|
||||
int reedState;
|
||||
int vibrationState;
|
||||
int lastVibration = LOW;
|
||||
bool motionActive = false;
|
||||
bool doorOpen = false;
|
||||
unsigned long motionStartTime = 0;
|
||||
unsigned long doorOpenTime = 0;
|
||||
unsigned long lastHeartbeat = 0;
|
||||
const unsigned long HEARTBEAT_INTERVAL = 300000;
|
||||
|
||||
// ============================================
|
||||
// LED
|
||||
// ============================================
|
||||
void setLED(bool bahaya) {
|
||||
digitalWrite(LED_MERAH, bahaya ? HIGH : LOW);
|
||||
digitalWrite(LED_HIJAU, bahaya ? LOW : HIGH);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// BUZZER
|
||||
// ============================================
|
||||
void buzzerPIR() {
|
||||
digitalWrite(BUZZER, HIGH);
|
||||
delay(300);
|
||||
digitalWrite(BUZZER, LOW);
|
||||
}
|
||||
|
||||
void buzzerREED() {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
digitalWrite(BUZZER, HIGH);
|
||||
delay(100);
|
||||
digitalWrite(BUZZER, LOW);
|
||||
delay(100);
|
||||
}
|
||||
}
|
||||
|
||||
void buzzerGETAR() {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
digitalWrite(BUZZER, HIGH);
|
||||
delay(80);
|
||||
digitalWrite(BUZZER, LOW);
|
||||
delay(80);
|
||||
}
|
||||
}
|
||||
|
||||
void buzzerStartup() {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
digitalWrite(BUZZER, HIGH);
|
||||
delay(200);
|
||||
digitalWrite(BUZZER, LOW);
|
||||
delay(100);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// WIFI
|
||||
// ============================================
|
||||
void setupWifi() {
|
||||
Serial.println("Menghubungkan WiFi: " + String(ssid));
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(ssid, password);
|
||||
|
||||
int attempt = 0;
|
||||
while (WiFi.status() != WL_CONNECTED && attempt < 30) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
attempt++;
|
||||
}
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.println("");
|
||||
Serial.println("WiFi Connected!");
|
||||
Serial.println("IP: " + WiFi.localIP().toString());
|
||||
} else {
|
||||
Serial.println("");
|
||||
Serial.println("WiFi gagal terhubung. Cek SSID/Password.");
|
||||
Serial.println("Pastikan hotspot 2.4GHz dan sudah aktif.");
|
||||
}
|
||||
}
|
||||
|
||||
void checkWifi() {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
Serial.println("WiFi putus, reconnecting...");
|
||||
WiFi.disconnect();
|
||||
delay(1000);
|
||||
WiFi.begin(ssid, password);
|
||||
|
||||
int attempt = 0;
|
||||
while (WiFi.status() != WL_CONNECTED && attempt < 20) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
attempt++;
|
||||
}
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.println("\nWiFi terhubung kembali! IP: " + WiFi.localIP().toString());
|
||||
} else {
|
||||
Serial.println("\nGagal reconnect. Tunggu 10 detik...");
|
||||
delay(10000); // tunggu lebih lama sebelum coba lagi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HTTP POST
|
||||
// ============================================
|
||||
bool httpPost(String endpoint, String jsonBody) {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
Serial.println("WiFi disconnected, skip kirim data");
|
||||
return false;
|
||||
}
|
||||
|
||||
HTTPClient http;
|
||||
String url = String(API_BASE) + endpoint;
|
||||
|
||||
http.begin(url);
|
||||
http.addHeader("Content-Type", "application/json");
|
||||
http.addHeader("Accept", "application/json");
|
||||
http.setTimeout(8000);
|
||||
|
||||
int httpCode = http.POST(jsonBody);
|
||||
|
||||
if (httpCode > 0) {
|
||||
String response = http.getString();
|
||||
Serial.println("HTTP " + String(httpCode) + " → " + endpoint);
|
||||
if (httpCode == 201 || httpCode == 200) {
|
||||
Serial.println("✓ Data tersimpan");
|
||||
http.end();
|
||||
return true;
|
||||
} else {
|
||||
Serial.println("✗ Response: " + response.substring(0, 150));
|
||||
}
|
||||
} else {
|
||||
Serial.println("✗ HTTP Error: " + String(httpCode));
|
||||
Serial.println(" URL: " + url);
|
||||
Serial.println(" Cek: server jalan? IP benar? Firewall?");
|
||||
}
|
||||
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// KIRIM PIR
|
||||
// ============================================
|
||||
void kirimDataPIR(bool detected, int intensity, int duration, String zone) {
|
||||
StaticJsonDocument<256> doc;
|
||||
doc["device_id"] = DEVICE_ID;
|
||||
doc["motion_detected"] = detected;
|
||||
doc["motion_intensity"] = intensity;
|
||||
doc["duration_seconds"] = duration;
|
||||
doc["detection_zone"] = zone;
|
||||
|
||||
String body;
|
||||
serializeJson(doc, body);
|
||||
Serial.println("Kirim PIR → intensity=" + String(intensity) + " duration=" + String(duration) + "s");
|
||||
httpPost("/pir/data", body);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// KIRIM GETARAN
|
||||
// ============================================
|
||||
void kirimDataGetaran(float x, float y, float z) {
|
||||
StaticJsonDocument<256> doc;
|
||||
doc["device_id"] = DEVICE_ID;
|
||||
doc["x_axis"] = x;
|
||||
doc["y_axis"] = y;
|
||||
doc["z_axis"] = z;
|
||||
doc["threshold"] = 2.0;
|
||||
|
||||
String body;
|
||||
serializeJson(doc, body);
|
||||
float mag = sqrt(x*x + y*y + z*z);
|
||||
Serial.println("Kirim Getaran → magnitude=" + String(mag, 2));
|
||||
httpPost("/vibration/data", body);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// KIRIM REED SWITCH
|
||||
// ============================================
|
||||
void kirimDataReedSwitch(bool opened, int duration) {
|
||||
StaticJsonDocument<256> doc;
|
||||
doc["device_id"] = DEVICE_ID;
|
||||
doc["door_opened"] = opened;
|
||||
doc["duration_seconds"] = duration;
|
||||
doc["access_method"] = "manual";
|
||||
doc["door_location"] = "rack";
|
||||
doc["is_forced_entry"] = false;
|
||||
|
||||
String body;
|
||||
serializeJson(doc, body);
|
||||
Serial.println("Kirim Reed Switch → opened=" + String(opened) + " duration=" + String(duration) + "s");
|
||||
httpPost("/door-access/data", body);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HEARTBEAT
|
||||
// ============================================
|
||||
void kirimHeartbeat() {
|
||||
String payload = "HEARTBEAT|" + NODE_ID;
|
||||
LoRa.beginPacket();
|
||||
LoRa.print(payload);
|
||||
LoRa.endPacket();
|
||||
Serial.println("Heartbeat terkirim. Uptime: " + String(millis()/1000) + "s");
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SETUP
|
||||
// ============================================
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
|
||||
// SENSOR
|
||||
pinMode(PIR_PIN, INPUT);
|
||||
pinMode(REED_PIN, INPUT_PULLUP); // PULLUP untuk reed switch
|
||||
pinMode(SW420_PIN, INPUT);
|
||||
|
||||
// OUTPUT
|
||||
pinMode(BUZZER, OUTPUT);
|
||||
pinMode(LED_MERAH, OUTPUT);
|
||||
pinMode(LED_HIJAU, OUTPUT);
|
||||
|
||||
digitalWrite(BUZZER, LOW);
|
||||
setLED(false); // LED hijau menyala default
|
||||
|
||||
Serial.println("=================================");
|
||||
Serial.println(" SMART RACK SECURITY SYSTEM");
|
||||
Serial.println("=================================");
|
||||
|
||||
// WIFI
|
||||
setupWifi();
|
||||
|
||||
// LORA
|
||||
LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0);
|
||||
if (!LoRa.begin(433E6)) {
|
||||
Serial.println("LoRa gagal! Lanjut tanpa LoRa.");
|
||||
} else {
|
||||
LoRa.setTxPower(17);
|
||||
Serial.println("LoRa OK");
|
||||
}
|
||||
|
||||
buzzerStartup();
|
||||
Serial.println("System Ready!");
|
||||
Serial.println("API: " + String(API_BASE));
|
||||
Serial.println("Device ID: " + String(DEVICE_ID));
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LOOP
|
||||
// ============================================
|
||||
void loop() {
|
||||
checkWifi();
|
||||
|
||||
pirState = digitalRead(PIR_PIN);
|
||||
reedState = digitalRead(REED_PIN); // LOW = tertutup, HIGH = terbuka
|
||||
vibrationState = digitalRead(SW420_PIN);
|
||||
|
||||
// =========================================
|
||||
// PIR - Deteksi Gerakan
|
||||
// =========================================
|
||||
if (pirState == HIGH && !motionActive) {
|
||||
motionActive = true;
|
||||
motionStartTime = millis();
|
||||
Serial.println(">>> PIR: Gerakan terdeteksi!");
|
||||
buzzerPIR();
|
||||
setLED(true);
|
||||
}
|
||||
else if (pirState == LOW && motionActive) {
|
||||
motionActive = false;
|
||||
int duration = (millis() - motionStartTime) / 1000;
|
||||
int intensity = random(60, 90);
|
||||
kirimDataPIR(true, intensity, duration, "center");
|
||||
Serial.println(">>> PIR: Gerakan selesai, durasi=" + String(duration) + "s");
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// REED SWITCH - Status Rak
|
||||
// =========================================
|
||||
if (reedState == HIGH && !doorOpen) {
|
||||
doorOpen = true;
|
||||
doorOpenTime = millis();
|
||||
Serial.println(">>> Reed Switch: Rak terbuka!");
|
||||
buzzerREED();
|
||||
setLED(true);
|
||||
}
|
||||
else if (reedState == LOW && doorOpen) {
|
||||
doorOpen = false;
|
||||
int duration = (millis() - doorOpenTime) / 1000;
|
||||
kirimDataReedSwitch(true, duration);
|
||||
Serial.println(">>> Reed Switch: Rak tertutup, durasi=" + String(duration) + "s");
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// SW-420 - Deteksi Getaran
|
||||
// =========================================
|
||||
if (vibrationState == HIGH && lastVibration == LOW) {
|
||||
Serial.println(">>> SW-420: Getaran terdeteksi!");
|
||||
float x = random(-300, 300) / 100.0;
|
||||
float y = random(-300, 300) / 100.0;
|
||||
float z = random(80, 120) / 100.0;
|
||||
buzzerGETAR();
|
||||
setLED(true);
|
||||
kirimDataGetaran(x, y, z);
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// Kembali normal jika tidak ada bahaya
|
||||
// =========================================
|
||||
if (!motionActive && !doorOpen && vibrationState == LOW) {
|
||||
setLED(false);
|
||||
}
|
||||
|
||||
lastVibration = vibrationState;
|
||||
|
||||
// =========================================
|
||||
// HEARTBEAT setiap 5 menit
|
||||
// =========================================
|
||||
if (millis() - lastHeartbeat >= HEARTBEAT_INTERVAL) {
|
||||
kirimHeartbeat();
|
||||
lastHeartbeat = millis();
|
||||
}
|
||||
|
||||
delay(300);
|
||||
}
|
||||
|
|
@ -0,0 +1,343 @@
|
|||
/**
|
||||
* ============================================
|
||||
* Smart Rack Security System
|
||||
* ESP32 GATEWAY (RECEIVER) — Di luar rak
|
||||
*
|
||||
* Fungsi : Terima data LoRa dari Node, forward ke Railway API
|
||||
* Komunikasi : LoRa SX1278 (RX) + WiFi (HTTPS ke Railway)
|
||||
* TIDAK ada sensor di sini
|
||||
* ============================================
|
||||
*/
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <SPI.h>
|
||||
#include <LoRa.h>
|
||||
|
||||
// ============================================
|
||||
// KONFIGURASI — SESUAIKAN
|
||||
// ============================================
|
||||
const char* WIFI_SSID = "gege";
|
||||
const char* WIFI_PASSWORD = "biasaaja";
|
||||
const char* API_BASE = "https://keamanan-rak-barang-production.up.railway.app/api";
|
||||
const int DEVICE_ID = 1; // ID device di database Railway
|
||||
|
||||
// ============================================
|
||||
// PIN LORA SX1278 RA-02
|
||||
// ============================================
|
||||
#define LORA_SS 27
|
||||
#define LORA_RST 14
|
||||
#define LORA_DIO0 26
|
||||
#define LORA_FREQ 433E6 // Harus sama dengan Node Sender!
|
||||
|
||||
// ============================================
|
||||
// PIN LED STATUS GATEWAY (opsional)
|
||||
// ============================================
|
||||
#define LED_WIFI 2 // LED bawaan ESP32 — nyala = WiFi OK
|
||||
#define LED_LORA 4 // Kedip = terima paket LoRa
|
||||
|
||||
// ============================================
|
||||
// STATISTIK
|
||||
// ============================================
|
||||
unsigned long totalReceived = 0;
|
||||
unsigned long totalSent = 0;
|
||||
unsigned long totalFailed = 0;
|
||||
unsigned long lastStatusPrint = 0;
|
||||
|
||||
// ============================================
|
||||
// SETUP WIFI
|
||||
// ============================================
|
||||
void setupWifi() {
|
||||
Serial.println("[WiFi] Menghubungkan ke: " + String(WIFI_SSID));
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||
|
||||
int attempt = 0;
|
||||
while (WiFi.status() != WL_CONNECTED && attempt < 40) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
attempt++;
|
||||
}
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.println("\n[WiFi] ✓ Terhubung! IP: " + WiFi.localIP().toString());
|
||||
digitalWrite(LED_WIFI, HIGH);
|
||||
} else {
|
||||
Serial.println("\n[WiFi] ✗ Gagal. Cek SSID/Password atau pastikan 2.4GHz.");
|
||||
digitalWrite(LED_WIFI, LOW);
|
||||
}
|
||||
}
|
||||
|
||||
void checkWifi() {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
Serial.println("[WiFi] Putus, reconnecting...");
|
||||
digitalWrite(LED_WIFI, LOW);
|
||||
WiFi.disconnect();
|
||||
delay(1000);
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||
|
||||
int attempt = 0;
|
||||
while (WiFi.status() != WL_CONNECTED && attempt < 20) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
attempt++;
|
||||
}
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.println("\n[WiFi] ✓ Terhubung kembali!");
|
||||
digitalWrite(LED_WIFI, HIGH);
|
||||
} else {
|
||||
Serial.println("\n[WiFi] ✗ Gagal reconnect.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HTTP POST KE RAILWAY API
|
||||
// ============================================
|
||||
bool httpPost(String endpoint, String jsonBody) {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
Serial.println("[HTTP] Skip — WiFi tidak terhubung");
|
||||
return false;
|
||||
}
|
||||
|
||||
WiFiClientSecure client;
|
||||
client.setInsecure(); // Skip SSL verify — cukup untuk Railway
|
||||
|
||||
HTTPClient http;
|
||||
String url = String(API_BASE) + endpoint;
|
||||
|
||||
http.begin(client, url);
|
||||
http.addHeader("Content-Type", "application/json");
|
||||
http.addHeader("Accept", "application/json");
|
||||
http.setTimeout(10000);
|
||||
|
||||
int httpCode = http.POST(jsonBody);
|
||||
|
||||
if (httpCode > 0) {
|
||||
if (httpCode == 200 || httpCode == 201) {
|
||||
Serial.println("[HTTP] ✓ " + endpoint + " → " + String(httpCode));
|
||||
http.end();
|
||||
totalSent++;
|
||||
return true;
|
||||
} else {
|
||||
String resp = http.getString();
|
||||
Serial.println("[HTTP] ✗ " + endpoint + " → " + String(httpCode));
|
||||
Serial.println(" " + resp.substring(0, 120));
|
||||
}
|
||||
} else {
|
||||
Serial.println("[HTTP] ✗ Error: " + String(httpCode) + " — " + endpoint);
|
||||
}
|
||||
|
||||
http.end();
|
||||
totalFailed++;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PROSES PAKET PIR
|
||||
// ============================================
|
||||
void prosesPIR(String nodeId, String jsonStr) {
|
||||
StaticJsonDocument<256> src;
|
||||
if (deserializeJson(src, jsonStr) != DeserializationError::Ok) {
|
||||
Serial.println("[PIR] JSON parse error");
|
||||
return;
|
||||
}
|
||||
|
||||
StaticJsonDocument<256> doc;
|
||||
doc["device_id"] = DEVICE_ID;
|
||||
doc["motion_detected"] = src["motion_detected"] | false;
|
||||
doc["motion_intensity"] = src["motion_intensity"] | 50;
|
||||
doc["duration_seconds"] = src["duration_seconds"] | 0;
|
||||
doc["detection_zone"] = src["detection_zone"] | "center";
|
||||
|
||||
String body;
|
||||
serializeJson(doc, body);
|
||||
Serial.println("[PIR] Node=" + nodeId + " → forward ke API");
|
||||
httpPost("/pir/data", body);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PROSES PAKET REED SWITCH
|
||||
// ============================================
|
||||
void prosesREED(String nodeId, String jsonStr) {
|
||||
StaticJsonDocument<256> src;
|
||||
if (deserializeJson(src, jsonStr) != DeserializationError::Ok) {
|
||||
Serial.println("[REED] JSON parse error");
|
||||
return;
|
||||
}
|
||||
|
||||
StaticJsonDocument<256> doc;
|
||||
doc["device_id"] = DEVICE_ID;
|
||||
doc["door_opened"] = src["door_opened"] | false;
|
||||
doc["duration_seconds"] = src["duration_seconds"] | 0;
|
||||
doc["access_method"] = src["access_method"] | "manual";
|
||||
doc["door_location"] = src["door_location"] | "rack";
|
||||
doc["is_forced_entry"] = src["is_forced_entry"] | false;
|
||||
|
||||
String body;
|
||||
serializeJson(doc, body);
|
||||
Serial.println("[REED] Node=" + nodeId + " → forward ke API");
|
||||
httpPost("/door-access/data", body);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PROSES PAKET GETARAN SW420
|
||||
// ============================================
|
||||
void prosesVIBRATION(String nodeId, String jsonStr) {
|
||||
StaticJsonDocument<256> src;
|
||||
if (deserializeJson(src, jsonStr) != DeserializationError::Ok) {
|
||||
Serial.println("[VIBRATION] JSON parse error");
|
||||
return;
|
||||
}
|
||||
|
||||
StaticJsonDocument<256> doc;
|
||||
doc["device_id"] = DEVICE_ID;
|
||||
doc["x_axis"] = src["x_axis"] | 0.0;
|
||||
doc["y_axis"] = src["y_axis"] | 0.0;
|
||||
doc["z_axis"] = src["z_axis"] | 0.0;
|
||||
doc["threshold"] = src["threshold"] | 2.0;
|
||||
|
||||
String body;
|
||||
serializeJson(doc, body);
|
||||
Serial.println("[VIBRATION] Node=" + nodeId + " → forward ke API");
|
||||
httpPost("/vibration/data", body);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PROSES PAKET HEARTBEAT
|
||||
// ============================================
|
||||
void prosesHEARTBEAT(String nodeId, String jsonStr) {
|
||||
StaticJsonDocument<128> src;
|
||||
deserializeJson(src, jsonStr);
|
||||
long uptime = src["uptime_s"] | 0;
|
||||
Serial.println("[HEARTBEAT] Node=" + nodeId + " masih hidup, uptime=" + String(uptime) + "s");
|
||||
|
||||
// Kirim heartbeat ke API agar device tetap Online di dashboard
|
||||
StaticJsonDocument<128> doc;
|
||||
doc["node_id"] = nodeId;
|
||||
doc["gateway_id"] = "GATEWAY_001";
|
||||
doc["payload"] = "HEARTBEAT|" + nodeId;
|
||||
|
||||
String body;
|
||||
serializeJson(doc, body);
|
||||
httpPost("/lora/receive", body);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PARSE & ROUTE PAKET LORA
|
||||
// Format paket: TYPE|NODE_ID|{json}
|
||||
// ============================================
|
||||
void prosesPacket(String packet, int rssi, float snr) {
|
||||
totalReceived++;
|
||||
|
||||
// Kedip LED tanda terima paket
|
||||
digitalWrite(LED_LORA, HIGH); delay(50); digitalWrite(LED_LORA, LOW);
|
||||
|
||||
Serial.println("─────────────────────────────────");
|
||||
Serial.println("[LoRa RX] RSSI=" + String(rssi) + " SNR=" + String(snr, 1));
|
||||
Serial.println("[LoRa RX] Raw: " + packet.substring(0, 100));
|
||||
|
||||
// Parse format: TYPE|NODE_ID|{json}
|
||||
int sep1 = packet.indexOf('|');
|
||||
if (sep1 < 0) {
|
||||
Serial.println("[LoRa RX] Format tidak dikenal, skip.");
|
||||
return;
|
||||
}
|
||||
|
||||
int sep2 = packet.indexOf('|', sep1 + 1);
|
||||
if (sep2 < 0) {
|
||||
Serial.println("[LoRa RX] Format tidak lengkap, skip.");
|
||||
return;
|
||||
}
|
||||
|
||||
String type = packet.substring(0, sep1);
|
||||
String nodeId = packet.substring(sep1 + 1, sep2);
|
||||
String payload = packet.substring(sep2 + 1);
|
||||
|
||||
Serial.println("[LoRa RX] Type=" + type + " Node=" + nodeId);
|
||||
|
||||
checkWifi(); // Pastikan WiFi masih konek sebelum forward
|
||||
|
||||
if (type == "PIR") prosesPIR(nodeId, payload);
|
||||
else if (type == "REED") prosesREED(nodeId, payload);
|
||||
else if (type == "VIBRATION") prosesVIBRATION(nodeId, payload);
|
||||
else if (type == "HEARTBEAT") prosesHEARTBEAT(nodeId, payload);
|
||||
else {
|
||||
Serial.println("[LoRa RX] Type tidak dikenal: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SETUP
|
||||
// ============================================
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
|
||||
pinMode(LED_WIFI, OUTPUT);
|
||||
pinMode(LED_LORA, OUTPUT);
|
||||
digitalWrite(LED_WIFI, LOW);
|
||||
digitalWrite(LED_LORA, LOW);
|
||||
|
||||
Serial.println("=================================");
|
||||
Serial.println(" SMART RACK — GATEWAY RECEIVER");
|
||||
Serial.println(" API: " + String(API_BASE));
|
||||
Serial.println(" Device ID: " + String(DEVICE_ID));
|
||||
Serial.println("=================================");
|
||||
|
||||
// WiFi
|
||||
setupWifi();
|
||||
|
||||
// LoRa — HARUS setting sama persis dengan Node Sender
|
||||
LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0);
|
||||
if (!LoRa.begin(LORA_FREQ)) {
|
||||
Serial.println("[LoRa] GAGAL INIT! Cek wiring.");
|
||||
while (true) {
|
||||
digitalWrite(LED_LORA, HIGH); delay(200);
|
||||
digitalWrite(LED_LORA, LOW); delay(200);
|
||||
}
|
||||
}
|
||||
|
||||
LoRa.setSpreadingFactor(9); // Harus sama dengan Node!
|
||||
LoRa.setSignalBandwidth(125E3);
|
||||
LoRa.setCodingRate4(5);
|
||||
|
||||
Serial.println("[LoRa] ✓ OK — " + String(LORA_FREQ / 1E6) + " MHz, menunggu paket...");
|
||||
Serial.println("Gateway siap!");
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LOOP
|
||||
// ============================================
|
||||
void loop() {
|
||||
// Cek apakah ada paket LoRa masuk
|
||||
int packetSize = LoRa.parsePacket();
|
||||
|
||||
if (packetSize > 0) {
|
||||
String packet = "";
|
||||
while (LoRa.available()) {
|
||||
packet += (char)LoRa.read();
|
||||
}
|
||||
int rssi = LoRa.packetRssi();
|
||||
float snr = LoRa.packetSnr();
|
||||
|
||||
prosesPacket(packet, rssi, snr);
|
||||
}
|
||||
|
||||
// Print statistik setiap 60 detik
|
||||
if (millis() - lastStatusPrint >= 60000) {
|
||||
Serial.println("─────────────────────────────────");
|
||||
Serial.println("[STATUS] Uptime: " + String(millis()/1000) + "s");
|
||||
Serial.println("[STATUS] LoRa diterima : " + String(totalReceived));
|
||||
Serial.println("[STATUS] API berhasil : " + String(totalSent));
|
||||
Serial.println("[STATUS] API gagal : " + String(totalFailed));
|
||||
Serial.println("[STATUS] WiFi: " + String(WiFi.status() == WL_CONNECTED ? "OK" : "PUTUS"));
|
||||
Serial.println("─────────────────────────────────");
|
||||
lastStatusPrint = millis();
|
||||
}
|
||||
|
||||
delay(10); // Kecil saja agar LoRa tidak miss paket
|
||||
}
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
/**
|
||||
* ============================================
|
||||
* Smart Rack Security System
|
||||
* ESP32 NODE (SENDER) — Di dalam / dekat rak
|
||||
*
|
||||
* Sensor : PIR + Reed Switch Magnetik + SW420
|
||||
* Output : Buzzer + LED Merah + LED Hijau
|
||||
* Komunikasi : LoRa SX1278 (TX only)
|
||||
* TIDAK butuh WiFi
|
||||
* ============================================
|
||||
*/
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include <SPI.h>
|
||||
#include <LoRa.h>
|
||||
#include <math.h>
|
||||
|
||||
// ============================================
|
||||
// IDENTITAS NODE
|
||||
// ============================================
|
||||
#define NODE_ID "NODE_001" // ID unik node ini
|
||||
|
||||
// ============================================
|
||||
// PIN SENSOR
|
||||
// ============================================
|
||||
#define PIR_PIN 33
|
||||
#define REED_PIN 32 // INPUT_PULLUP — LOW = tertutup, HIGH = terbuka
|
||||
#define SW420_PIN 34
|
||||
|
||||
// ============================================
|
||||
// PIN OUTPUT
|
||||
// ============================================
|
||||
#define BUZZER 25
|
||||
#define LED_MERAH 13
|
||||
#define LED_HIJAU 4
|
||||
|
||||
// ============================================
|
||||
// PIN LORA SX1278 RA-02
|
||||
// ============================================
|
||||
#define LORA_SS 27
|
||||
#define LORA_RST 14
|
||||
#define LORA_DIO0 26
|
||||
#define LORA_FREQ 433E6 // 433 MHz — sesuaikan modul kamu
|
||||
|
||||
// ============================================
|
||||
// VARIABEL STATE SENSOR
|
||||
// ============================================
|
||||
int pirState = LOW;
|
||||
int reedState = LOW;
|
||||
int vibrationState = LOW;
|
||||
int lastVibration = LOW;
|
||||
|
||||
bool motionActive = false;
|
||||
bool doorOpen = false;
|
||||
|
||||
unsigned long motionStartTime = 0;
|
||||
unsigned long doorOpenTime = 0;
|
||||
unsigned long lastHeartbeat = 0;
|
||||
|
||||
// Heartbeat ke gateway setiap 2 menit
|
||||
const unsigned long HEARTBEAT_INTERVAL = 120000;
|
||||
|
||||
// ============================================
|
||||
// LED
|
||||
// ============================================
|
||||
void setLED(bool bahaya) {
|
||||
digitalWrite(LED_MERAH, bahaya ? HIGH : LOW);
|
||||
digitalWrite(LED_HIJAU, bahaya ? LOW : HIGH);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// BUZZER
|
||||
// ============================================
|
||||
void buzzerPIR() {
|
||||
digitalWrite(BUZZER, HIGH); delay(300);
|
||||
digitalWrite(BUZZER, LOW);
|
||||
}
|
||||
|
||||
void buzzerREED() {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
digitalWrite(BUZZER, HIGH); delay(100);
|
||||
digitalWrite(BUZZER, LOW); delay(100);
|
||||
}
|
||||
}
|
||||
|
||||
void buzzerGETAR() {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
digitalWrite(BUZZER, HIGH); delay(80);
|
||||
digitalWrite(BUZZER, LOW); delay(80);
|
||||
}
|
||||
}
|
||||
|
||||
void buzzerStartup() {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
digitalWrite(BUZZER, HIGH); delay(200);
|
||||
digitalWrite(BUZZER, LOW); delay(100);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// KIRIM PAKET LORA
|
||||
// Format: TYPE|NODE_ID|json_payload
|
||||
// ============================================
|
||||
bool loRaSend(String type, String jsonPayload) {
|
||||
String packet = type + "|" + NODE_ID + "|" + jsonPayload;
|
||||
|
||||
LoRa.beginPacket();
|
||||
LoRa.print(packet);
|
||||
int result = LoRa.endPacket();
|
||||
|
||||
if (result) {
|
||||
Serial.println("[LoRa TX] ✓ " + packet.substring(0, 80));
|
||||
} else {
|
||||
Serial.println("[LoRa TX] ✗ Gagal kirim paket");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// KIRIM DATA PIR
|
||||
// ============================================
|
||||
void kirimPIR(bool detected, int intensity, int duration) {
|
||||
StaticJsonDocument<200> doc;
|
||||
doc["motion_detected"] = detected;
|
||||
doc["motion_intensity"] = intensity;
|
||||
doc["duration_seconds"] = duration;
|
||||
doc["detection_zone"] = "center";
|
||||
|
||||
String body;
|
||||
serializeJson(doc, body);
|
||||
Serial.println("[PIR] detected=" + String(detected) + " intensity=" + String(intensity) + " dur=" + String(duration) + "s");
|
||||
loRaSend("PIR", body);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// KIRIM DATA REED SWITCH
|
||||
// ============================================
|
||||
void kirimReed(bool opened, int duration) {
|
||||
StaticJsonDocument<200> doc;
|
||||
doc["door_opened"] = opened;
|
||||
doc["duration_seconds"] = duration;
|
||||
doc["access_method"] = "manual";
|
||||
doc["door_location"] = "rack";
|
||||
doc["is_forced_entry"] = false;
|
||||
|
||||
String body;
|
||||
serializeJson(doc, body);
|
||||
Serial.println("[REED] opened=" + String(opened) + " dur=" + String(duration) + "s");
|
||||
loRaSend("REED", body);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// KIRIM DATA GETARAN SW420
|
||||
// ============================================
|
||||
void kirimGetaran(float x, float y, float z) {
|
||||
StaticJsonDocument<200> doc;
|
||||
doc["x_axis"] = x;
|
||||
doc["y_axis"] = y;
|
||||
doc["z_axis"] = z;
|
||||
doc["threshold"] = 2.0;
|
||||
|
||||
String body;
|
||||
serializeJson(doc, body);
|
||||
float mag = sqrt(x*x + y*y + z*z);
|
||||
Serial.println("[SW420] magnitude=" + String(mag, 2));
|
||||
loRaSend("VIBRATION", body);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HEARTBEAT — beri tahu gateway node masih hidup
|
||||
// ============================================
|
||||
void kirimHeartbeat() {
|
||||
StaticJsonDocument<64> doc;
|
||||
doc["uptime_s"] = millis() / 1000;
|
||||
|
||||
String body;
|
||||
serializeJson(doc, body);
|
||||
Serial.println("[HEARTBEAT] uptime=" + String(millis()/1000) + "s");
|
||||
loRaSend("HEARTBEAT", body);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SETUP
|
||||
// ============================================
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
|
||||
// Pin sensor
|
||||
pinMode(PIR_PIN, INPUT);
|
||||
pinMode(REED_PIN, INPUT_PULLUP);
|
||||
pinMode(SW420_PIN, INPUT);
|
||||
|
||||
// Pin output
|
||||
pinMode(BUZZER, OUTPUT);
|
||||
pinMode(LED_MERAH, OUTPUT);
|
||||
pinMode(LED_HIJAU, OUTPUT);
|
||||
|
||||
digitalWrite(BUZZER, LOW);
|
||||
setLED(false); // LED hijau nyala default
|
||||
|
||||
Serial.println("=================================");
|
||||
Serial.println(" SMART RACK — NODE SENDER");
|
||||
Serial.println(" Node ID : " NODE_ID);
|
||||
Serial.println("=================================");
|
||||
|
||||
// Init LoRa
|
||||
LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0);
|
||||
if (!LoRa.begin(LORA_FREQ)) {
|
||||
Serial.println("[LoRa] GAGAL INIT! Cek wiring.");
|
||||
// Blink LED merah terus sebagai tanda error
|
||||
while (true) {
|
||||
digitalWrite(LED_MERAH, HIGH); delay(200);
|
||||
digitalWrite(LED_MERAH, LOW); delay(200);
|
||||
}
|
||||
}
|
||||
|
||||
LoRa.setTxPower(17);
|
||||
LoRa.setSpreadingFactor(9); // SF9 — balance range vs speed
|
||||
LoRa.setSignalBandwidth(125E3);
|
||||
LoRa.setCodingRate4(5);
|
||||
|
||||
Serial.println("[LoRa] OK — " + String(LORA_FREQ / 1E6) + " MHz");
|
||||
|
||||
buzzerStartup();
|
||||
Serial.println("Node siap!");
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LOOP
|
||||
// ============================================
|
||||
void loop() {
|
||||
pirState = digitalRead(PIR_PIN);
|
||||
reedState = digitalRead(REED_PIN);
|
||||
vibrationState = digitalRead(SW420_PIN);
|
||||
|
||||
// =========================================
|
||||
// PIR — Deteksi Gerakan
|
||||
// =========================================
|
||||
if (pirState == HIGH && !motionActive) {
|
||||
motionActive = true;
|
||||
motionStartTime = millis();
|
||||
Serial.println(">>> PIR: Gerakan terdeteksi!");
|
||||
buzzerPIR();
|
||||
setLED(true);
|
||||
}
|
||||
else if (pirState == LOW && motionActive) {
|
||||
motionActive = false;
|
||||
int duration = (millis() - motionStartTime) / 1000;
|
||||
int intensity = random(60, 90); // estimasi intensitas
|
||||
kirimPIR(true, intensity, duration);
|
||||
Serial.println(">>> PIR: Selesai, durasi=" + String(duration) + "s");
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// REED SWITCH — Status Rak
|
||||
// =========================================
|
||||
if (reedState == HIGH && !doorOpen) {
|
||||
doorOpen = true;
|
||||
doorOpenTime = millis();
|
||||
Serial.println(">>> REED: Rak terbuka!");
|
||||
buzzerREED();
|
||||
setLED(true);
|
||||
}
|
||||
else if (reedState == LOW && doorOpen) {
|
||||
doorOpen = false;
|
||||
int duration = (millis() - doorOpenTime) / 1000;
|
||||
kirimReed(true, duration);
|
||||
Serial.println(">>> REED: Rak tertutup, durasi=" + String(duration) + "s");
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// SW-420 — Deteksi Getaran
|
||||
// =========================================
|
||||
if (vibrationState == HIGH && lastVibration == LOW) {
|
||||
Serial.println(">>> SW420: Getaran terdeteksi!");
|
||||
// Simulasi nilai axis — ganti dengan sensor akselerometer nyata jika ada
|
||||
float x = random(-300, 300) / 100.0;
|
||||
float y = random(-300, 300) / 100.0;
|
||||
float z = random(80, 120) / 100.0;
|
||||
buzzerGETAR();
|
||||
setLED(true);
|
||||
kirimGetaran(x, y, z);
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// Reset LED jika semua aman
|
||||
// =========================================
|
||||
if (!motionActive && !doorOpen && vibrationState == LOW) {
|
||||
setLED(false);
|
||||
}
|
||||
|
||||
lastVibration = vibrationState;
|
||||
|
||||
// =========================================
|
||||
// HEARTBEAT setiap 2 menit
|
||||
// =========================================
|
||||
if (millis() - lastHeartbeat >= HEARTBEAT_INTERVAL) {
|
||||
kirimHeartbeat();
|
||||
lastHeartbeat = millis();
|
||||
}
|
||||
|
||||
delay(300);
|
||||
}
|
||||
|
|
@ -22,25 +22,44 @@
|
|||
<!-- NAVBAR -->
|
||||
<nav class="glass fixed top-0 w-full z-50 border-b border-white/10">
|
||||
<div class="max-w-7xl mx-auto flex justify-between items-center px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
|
||||
<i data-feather="shield" class="w-5 h-5 text-white"></i>
|
||||
</div>
|
||||
<h1 class="text-xl font-display font-bold text-gray-800">Smart Rack Security</h1>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
|
||||
<i data-feather="shield" class="w-5 h-5 text-white"></i>
|
||||
</div>
|
||||
<h1 class="text-xl font-display font-bold text-gray-800">Smart Rack Security</h1>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center gap-8">
|
||||
<a href="/home" class="text-gray-700 hover:text-indigo-600 transition-colors font-medium">Home</a>
|
||||
<a href="#fitur" class="text-gray-700 hover:text-indigo-600 transition-colors font-medium">Sensor</a>
|
||||
<a href="#tentang" class="text-gray-700 hover:text-indigo-600 transition-colors font-medium">Tentang</a>
|
||||
<a href="/login" class="bg-gradient-to-r from-indigo-600 to-purple-600 text-white px-6 py-2.5 rounded-xl hover:shadow-lg hover:scale-105 transition-all duration-300 font-medium">
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Hamburger button -->
|
||||
<button id="hamburger-btn" onclick="toggleMobileMenu()" class="md:hidden p-2 rounded-lg hover:bg-white/20 transition-colors focus:outline-none" aria-label="Toggle menu">
|
||||
<i id="hamburger-icon" data-feather="menu" class="w-6 h-6 text-gray-800"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center gap-8">
|
||||
<a href="/home" class="text-gray-700 hover:text-indigo-600 transition-colors font-medium">Home</a>
|
||||
<a href="#fitur" class="text-gray-700 hover:text-indigo-600 transition-colors font-medium">Sensor</a>
|
||||
<a href="#tentang" class="text-gray-700 hover:text-indigo-600 transition-colors font-medium">Tentang</a>
|
||||
<a href="/login" class="bg-gradient-to-r from-indigo-600 to-purple-600 text-white px-6 py-2.5 rounded-xl hover:shadow-lg hover:scale-105 transition-all duration-300 font-medium">
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button class="md:hidden p-2 rounded-lg hover:bg-white/20 transition-colors">
|
||||
<i data-feather="menu" class="w-6 h-6 text-gray-800"></i>
|
||||
</button>
|
||||
<!-- Mobile Menu -->
|
||||
<div id="mobile-menu" class="hidden md:hidden border-t border-white/20 bg-white/95 backdrop-blur-md">
|
||||
<div class="max-w-7xl mx-auto px-6 py-4 flex flex-col gap-1">
|
||||
<a href="/home" class="flex items-center gap-3 px-4 py-3 rounded-xl text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 transition-all font-medium">
|
||||
<i data-feather="home" class="w-5 h-5"></i> Home
|
||||
</a>
|
||||
<a href="#fitur" onclick="closeMobileMenu()" class="flex items-center gap-3 px-4 py-3 rounded-xl text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 transition-all font-medium">
|
||||
<i data-feather="layers" class="w-5 h-5"></i> Sensor
|
||||
</a>
|
||||
<a href="#tentang" onclick="closeMobileMenu()" class="flex items-center gap-3 px-4 py-3 rounded-xl text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 transition-all font-medium">
|
||||
<i data-feather="info" class="w-5 h-5"></i> Tentang
|
||||
</a>
|
||||
<a href="/login" class="flex items-center justify-center gap-3 mt-2 px-4 py-3 rounded-xl bg-gradient-to-r from-indigo-600 to-purple-600 text-white font-semibold hover:shadow-lg transition-all">
|
||||
<i data-feather="log-in" class="w-5 h-5"></i> Login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
|
@ -331,5 +350,36 @@
|
|||
feather.replace()
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function toggleMobileMenu() {
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
const icon = document.getElementById('hamburger-icon');
|
||||
const isOpen = !menu.classList.contains('hidden');
|
||||
|
||||
if (isOpen) {
|
||||
menu.classList.add('hidden');
|
||||
icon.setAttribute('data-feather', 'menu');
|
||||
} else {
|
||||
menu.classList.remove('hidden');
|
||||
icon.setAttribute('data-feather', 'x');
|
||||
}
|
||||
feather.replace();
|
||||
}
|
||||
|
||||
function closeMobileMenu() {
|
||||
document.getElementById('mobile-menu').classList.add('hidden');
|
||||
document.getElementById('hamburger-icon').setAttribute('data-feather', 'menu');
|
||||
feather.replace();
|
||||
}
|
||||
|
||||
// Tutup menu saat klik di luar navbar
|
||||
document.addEventListener('click', function (e) {
|
||||
const nav = document.querySelector('nav');
|
||||
if (!nav.contains(e.target)) {
|
||||
closeMobileMenu();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue