Compare commits
10 Commits
181c619cb1
...
4539200a5b
| Author | SHA1 | Date |
|---|---|---|
|
|
4539200a5b | |
|
|
fc01ea9ceb | |
|
|
d53ed16046 | |
|
|
74d576eb4d | |
|
|
4e3d5a6902 | |
|
|
3b30ccf2be | |
|
|
2bc2c70da3 | |
|
|
3be34eeea9 | |
|
|
d17918ab61 | |
|
|
3846619292 |
|
|
@ -123,6 +123,20 @@ public function receiveData(Request $request): JsonResponse
|
|||
// Tandai device sebagai online
|
||||
Device::markOnline($data['device_id']);
|
||||
|
||||
// Tulis activity log
|
||||
$sensor = \App\Models\Sensor::where('device_id', $data['device_id'])->where('type', 'reed_switch')->first();
|
||||
\App\Models\ActivityLog::create([
|
||||
'device_id' => $data['device_id'],
|
||||
'sensor_id' => $sensor?->id,
|
||||
'event_type' => $data['door_opened'] ? 'door_opened' : 'door_closed',
|
||||
'severity' => $isSuspicious ? 'warning' : 'info',
|
||||
'title' => $data['door_opened'] ? 'Rak Dibuka' : 'Rak Ditutup',
|
||||
'description'=> "Reed switch mendeteksi rak " . ($data['door_opened'] ? 'dibuka' : 'ditutup') . " di lokasi " . ($data['door_location'] ?? 'rack') . ". Durasi: {$durationSeconds}s.",
|
||||
'event_data' => ['door_reading_id' => $doorReading->id, 'access_type' => $accessType, 'duration' => $durationSeconds],
|
||||
'location' => $sensor?->device?->location ?? 'Rak A - Lantai 1',
|
||||
'event_time' => $recordedAt,
|
||||
]);
|
||||
|
||||
// Jika akses mencurigakan atau tidak sah, buat alert dan kirim notifikasi
|
||||
$alertSent = false;
|
||||
if ($isSuspicious || !$isAuthorizedAccess || $accessType === 'forced_entry') {
|
||||
|
|
|
|||
|
|
@ -112,6 +112,22 @@ public function receiveData(Request $request): JsonResponse
|
|||
// Tandai device sebagai online
|
||||
Device::markOnline($data['device_id']);
|
||||
|
||||
// Tulis activity log
|
||||
if ($data['motion_detected']) {
|
||||
$sensor = \App\Models\Sensor::where('device_id', $data['device_id'])->where('type', 'pir')->first();
|
||||
\App\Models\ActivityLog::create([
|
||||
'device_id' => $data['device_id'],
|
||||
'sensor_id' => $sensor?->id,
|
||||
'event_type' => $isSuspicious ? 'motion_detected' : 'motion_detected',
|
||||
'severity' => $isSuspicious ? 'warning' : 'info',
|
||||
'title' => $isSuspicious ? 'Gerakan Mencurigakan Terdeteksi' : 'Gerakan Terdeteksi',
|
||||
'description'=> "Sensor PIR mendeteksi gerakan di zona {$pirReading->detection_zone}. Intensitas: {$motionIntensity}%.",
|
||||
'event_data' => ['pir_reading_id' => $pirReading->id, 'motion_type' => $motionType, 'intensity' => $motionIntensity],
|
||||
'location' => $sensor?->device?->location ?? 'Rak A - Lantai 1',
|
||||
'event_time' => $recordedAt,
|
||||
]);
|
||||
}
|
||||
|
||||
// Jika gerakan mencurigakan, buat alert dan kirim notifikasi
|
||||
$alertSent = false;
|
||||
if ($isSuspicious || $motionType === 'unauthorized') {
|
||||
|
|
|
|||
|
|
@ -88,6 +88,20 @@ public function receiveData(Request $request): JsonResponse
|
|||
// Tandai device sebagai online
|
||||
Device::markOnline($data['device_id']);
|
||||
|
||||
// Tulis activity log
|
||||
$sensor = \App\Models\Sensor::where('device_id', $data['device_id'])->where('type', 'vibration')->first();
|
||||
\App\Models\ActivityLog::create([
|
||||
'device_id' => $data['device_id'],
|
||||
'sensor_id' => $sensor?->id,
|
||||
'event_type' => $isAbnormal ? 'vibration_detected' : 'system_normal',
|
||||
'severity' => $status === 'critical' ? 'critical' : ($isAbnormal ? 'warning' : 'info'),
|
||||
'title' => $isAbnormal ? 'Getaran Abnormal Terdeteksi' : 'Getaran Normal',
|
||||
'description'=> "Sensor SW-420 mendeteksi getaran. Magnitude: " . round($magnitude, 2) . ", Status: {$status}.",
|
||||
'event_data' => ['vibration_reading_id' => $vibrationReading->id, 'magnitude' => $magnitude, 'status' => $status],
|
||||
'location' => $sensor?->device?->location ?? 'Rak A - Lantai 1',
|
||||
'event_time' => now(),
|
||||
]);
|
||||
|
||||
// Jika getaran abnormal, buat alert dan kirim notifikasi
|
||||
if ($isAbnormal) {
|
||||
$this->handleAbnormalVibration($vibrationReading);
|
||||
|
|
|
|||
|
|
@ -1,42 +1,89 @@
|
|||
/**
|
||||
* ============================================
|
||||
* 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>
|
||||
#include <PubSubClient.h>
|
||||
|
||||
// ============================================
|
||||
// KONFIGURASI — SESUAIKAN
|
||||
// API (Railway)
|
||||
// ============================================
|
||||
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
|
||||
const char* API_BASE = "https://keamanan-rak-barang-production.up.railway.app/api";
|
||||
const int DEVICE_ID = 1;
|
||||
|
||||
// ============================================
|
||||
// PIN LORA SX1278 RA-02
|
||||
// WIFI
|
||||
// ============================================
|
||||
#define LORA_SS 27
|
||||
#define LORA_RST 14
|
||||
#define LORA_DIO0 26
|
||||
#define LORA_FREQ 433E6 // Harus sama dengan Node Sender!
|
||||
const char* WIFI_SSID = "ini";
|
||||
const char* WIFI_PASS = "00000000";
|
||||
|
||||
// ============================================
|
||||
// PIN LED STATUS GATEWAY (opsional)
|
||||
// MQTT HiveMQ
|
||||
// ============================================
|
||||
#define LED_WIFI 2 // LED bawaan ESP32 — nyala = WiFi OK
|
||||
#define LED_LORA 4 // Kedip = terima paket LoRa
|
||||
const char* MQTT_SERVER = "broker.hivemq.com";
|
||||
const int MQTT_PORT = 1883;
|
||||
const char* TOPIC_PIR = "keamanan/pir";
|
||||
const char* TOPIC_REED = "keamanan/reed";
|
||||
const char* TOPIC_VIBRATION = "keamanan/vibration";
|
||||
const char* TOPIC_STATUS = "keamanan/status";
|
||||
const char* TOPIC_TEST = "keamanan/test";
|
||||
|
||||
WiFiClient espClient;
|
||||
PubSubClient mqtt(espClient);
|
||||
|
||||
// ============================================
|
||||
// LORA CONFIG
|
||||
// ============================================
|
||||
#define LORA_SS 27
|
||||
#define LORA_RST 14
|
||||
#define LORA_DIO0 26
|
||||
#define LORA_FREQ 433E6
|
||||
#define LORA_SYNC_WORD 0xA5
|
||||
#define LORA_SF 9
|
||||
#define LORA_BW 125E3
|
||||
#define LORA_CR 5
|
||||
|
||||
#define EXPECTED_NODE "NODE_001"
|
||||
#define EXPECTED_GATEWAY "GATEWAY_001"
|
||||
|
||||
// ============================================
|
||||
// LED
|
||||
// ============================================
|
||||
#define LED_WIFI 2
|
||||
#define LED_LORA 4
|
||||
#define LED_HIJAU 33
|
||||
#define LED_MERAH 25
|
||||
|
||||
// ============================================
|
||||
// BUZZER
|
||||
// ============================================
|
||||
#define PIN_BUZZER 32 // Ganti sesuai pin yang tersedia di ESP32 gateway
|
||||
|
||||
// Pola bunyi per sensor
|
||||
void buzzerPIR() { // 3x beep pendek
|
||||
for (int i = 0; i < 3; i++) {
|
||||
digitalWrite(PIN_BUZZER, HIGH); delay(80);
|
||||
digitalWrite(PIN_BUZZER, LOW); delay(80);
|
||||
}
|
||||
}
|
||||
void buzzerReed() { // 1x beep panjang
|
||||
digitalWrite(PIN_BUZZER, HIGH); delay(600);
|
||||
digitalWrite(PIN_BUZZER, LOW);
|
||||
}
|
||||
void buzzerVib() { // 2x beep sedang
|
||||
for (int i = 0; i < 2; i++) {
|
||||
digitalWrite(PIN_BUZZER, HIGH); delay(150);
|
||||
digitalWrite(PIN_BUZZER, LOW); delay(100);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ALERT SYSTEM
|
||||
// ============================================
|
||||
#define ALERT_HOLD 3000
|
||||
bool alertActive = false;
|
||||
unsigned long alertStartTime = 0;
|
||||
|
||||
// ============================================
|
||||
// STATISTIK
|
||||
|
|
@ -47,226 +94,313 @@ unsigned long totalFailed = 0;
|
|||
unsigned long lastStatusPrint = 0;
|
||||
|
||||
// ============================================
|
||||
// SETUP WIFI
|
||||
// BLINK LORA LED
|
||||
// ============================================
|
||||
void blinkLoRa() {
|
||||
digitalWrite(LED_LORA, HIGH);
|
||||
delay(50);
|
||||
digitalWrite(LED_LORA, LOW);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// WIFI CONNECT
|
||||
// ============================================
|
||||
void setupWifi() {
|
||||
Serial.println("[WiFi] Menghubungkan ke: " + String(WIFI_SSID));
|
||||
Serial.println("\n[WiFi] Connecting...");
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
||||
|
||||
int attempt = 0;
|
||||
while (WiFi.status() != WL_CONNECTED && attempt < 40) {
|
||||
int retry = 0;
|
||||
while (WiFi.status() != WL_CONNECTED && retry < 40) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
attempt++;
|
||||
retry++;
|
||||
}
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.println("\n[WiFi] ✓ Terhubung! IP: " + WiFi.localIP().toString());
|
||||
Serial.println("\n[WiFi] CONNECTED 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);
|
||||
Serial.println("\n[WiFi] FAILED — restart");
|
||||
ESP.restart();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// WIFI AUTO RECONNECT
|
||||
// ============================================
|
||||
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);
|
||||
if (WiFi.status() == WL_CONNECTED) return;
|
||||
Serial.println("[WiFi] Reconnecting...");
|
||||
digitalWrite(LED_WIFI, LOW);
|
||||
WiFi.disconnect();
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
||||
|
||||
int attempt = 0;
|
||||
while (WiFi.status() != WL_CONNECTED && attempt < 20) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
attempt++;
|
||||
}
|
||||
int retry = 0;
|
||||
while (WiFi.status() != WL_CONNECTED && retry < 20) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
retry++;
|
||||
}
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.println("\n[WiFi] ✓ Terhubung kembali!");
|
||||
digitalWrite(LED_WIFI, HIGH);
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.println("\n[WiFi] RECONNECTED");
|
||||
digitalWrite(LED_WIFI, HIGH);
|
||||
} else {
|
||||
Serial.println("\n[WiFi] FAILED RECONNECT");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MQTT CONNECT
|
||||
// ============================================
|
||||
void connectMQTT() {
|
||||
mqtt.setServer(MQTT_SERVER, MQTT_PORT);
|
||||
while (!mqtt.connected()) {
|
||||
String clientId = "ESP32-GW-" + String(random(0xffff), HEX);
|
||||
Serial.println("[MQTT] Connecting...");
|
||||
if (mqtt.connect(clientId.c_str())) {
|
||||
Serial.println("[MQTT] CONNECTED");
|
||||
} else {
|
||||
Serial.println("\n[WiFi] ✗ Gagal reconnect.");
|
||||
Serial.print("[MQTT] FAILED rc=");
|
||||
Serial.println(mqtt.state());
|
||||
delay(2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HTTP POST KE RAILWAY API
|
||||
// MQTT PUBLISH
|
||||
// ============================================
|
||||
bool httpPost(String endpoint, String jsonBody) {
|
||||
void publishMQTT(const char* topic, String payload) {
|
||||
if (!mqtt.connected()) return;
|
||||
mqtt.publish(topic, payload.c_str());
|
||||
Serial.println("[MQTT] SENT -> " + String(topic));
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HTTP POST
|
||||
// ============================================
|
||||
bool httpPost(String endpoint, String body) {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
Serial.println("[HTTP] Skip — WiFi tidak terhubung");
|
||||
return false;
|
||||
}
|
||||
|
||||
WiFiClientSecure client;
|
||||
client.setInsecure(); // Skip SSL verify — cukup untuk Railway
|
||||
client.setInsecure();
|
||||
|
||||
HTTPClient http;
|
||||
String url = String(API_BASE) + endpoint;
|
||||
|
||||
Serial.println("[HTTP] POST -> " + url);
|
||||
http.begin(client, url);
|
||||
http.addHeader("Content-Type", "application/json");
|
||||
http.addHeader("Accept", "application/json");
|
||||
http.setTimeout(10000);
|
||||
|
||||
int httpCode = http.POST(jsonBody);
|
||||
int code = http.POST(body);
|
||||
Serial.print("[HTTP] CODE: ");
|
||||
Serial.println(code);
|
||||
|
||||
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);
|
||||
if (code != 200 && code != 201) {
|
||||
String resp = http.getString();
|
||||
Serial.println("[HTTP] RESP: " + resp.substring(0, 150));
|
||||
}
|
||||
|
||||
http.end();
|
||||
|
||||
if (code == 200 || code == 201) {
|
||||
totalSent++;
|
||||
return true;
|
||||
}
|
||||
totalFailed++;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PROSES PAKET PIR
|
||||
// ALERT
|
||||
// ============================================
|
||||
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);
|
||||
void setAlert(String msg) {
|
||||
digitalWrite(LED_MERAH, HIGH);
|
||||
digitalWrite(LED_HIJAU, LOW);
|
||||
alertActive = true;
|
||||
alertStartTime = millis();
|
||||
Serial.println("[ALERT] " + msg);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PROSES PAKET REED SWITCH
|
||||
// ROUTER CORE
|
||||
// Terima JSON mentah dari node sender, parse,
|
||||
// lalu kirim ke API dengan format yang benar.
|
||||
// ============================================
|
||||
void prosesREED(String nodeId, String jsonStr) {
|
||||
StaticJsonDocument<256> src;
|
||||
if (deserializeJson(src, jsonStr) != DeserializationError::Ok) {
|
||||
Serial.println("[REED] JSON parse error");
|
||||
void routePacket(String raw) {
|
||||
StaticJsonDocument<512> doc;
|
||||
DeserializationError err = deserializeJson(doc, raw);
|
||||
if (err) {
|
||||
Serial.println("[JSON] INVALID: " + String(err.c_str()));
|
||||
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 node = doc["node_id"] | "";
|
||||
String gateway = doc["gateway_id"] | "";
|
||||
String type = doc["type"] | "";
|
||||
|
||||
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");
|
||||
// Filter node & gateway
|
||||
if (node != EXPECTED_NODE) {
|
||||
Serial.println("[FILTER] INVALID NODE: " + node);
|
||||
return;
|
||||
}
|
||||
if (gateway != EXPECTED_GATEWAY) {
|
||||
Serial.println("[FILTER] INVALID GATEWAY: " + gateway);
|
||||
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;
|
||||
Serial.println("[ROUTE] type=" + type + " node=" + node);
|
||||
checkWifi();
|
||||
|
||||
String body;
|
||||
serializeJson(doc, body);
|
||||
Serial.println("[VIBRATION] Node=" + nodeId + " → forward ke API");
|
||||
httpPost("/vibration/data", body);
|
||||
}
|
||||
// =========================================
|
||||
// PIR
|
||||
// =========================================
|
||||
if (type == "PIR") {
|
||||
setAlert("PIR detected");
|
||||
buzzerPIR(); // bunyi di gateway
|
||||
|
||||
// ============================================
|
||||
// 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");
|
||||
// Build payload untuk API
|
||||
StaticJsonDocument<256> apiDoc;
|
||||
apiDoc["device_id"] = DEVICE_ID;
|
||||
apiDoc["motion_detected"] = doc["motion_detected"] | false;
|
||||
apiDoc["motion_intensity"] = doc["motion_intensity"] | 50;
|
||||
apiDoc["duration_seconds"] = doc["duration_seconds"] | 0;
|
||||
apiDoc["detection_zone"] = doc["detection_zone"] | "center";
|
||||
String apiBody;
|
||||
serializeJson(apiDoc, apiBody);
|
||||
httpPost("/pir/data", apiBody);
|
||||
|
||||
// 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;
|
||||
// Build payload untuk MQTT
|
||||
StaticJsonDocument<256> mqttDoc;
|
||||
mqttDoc["device_id"] = DEVICE_ID;
|
||||
mqttDoc["node"] = node;
|
||||
mqttDoc["type"] = type;
|
||||
mqttDoc["motion"] = doc["motion_detected"] | false;
|
||||
String mqttBody;
|
||||
serializeJson(mqttDoc, mqttBody);
|
||||
publishMQTT(TOPIC_PIR, mqttBody);
|
||||
}
|
||||
|
||||
int sep2 = packet.indexOf('|', sep1 + 1);
|
||||
if (sep2 < 0) {
|
||||
Serial.println("[LoRa RX] Format tidak lengkap, skip.");
|
||||
return;
|
||||
// =========================================
|
||||
// REED
|
||||
// =========================================
|
||||
else if (type == "REED") {
|
||||
setAlert("DOOR event");
|
||||
buzzerReed(); // bunyi di gateway
|
||||
|
||||
// Build payload untuk API
|
||||
StaticJsonDocument<256> apiDoc;
|
||||
apiDoc["device_id"] = DEVICE_ID;
|
||||
apiDoc["door_opened"] = doc["door_opened"] | false;
|
||||
apiDoc["duration_seconds"] = doc["duration_seconds"] | 0;
|
||||
apiDoc["access_method"] = doc["access_method"] | "manual";
|
||||
apiDoc["door_location"] = doc["door_location"] | "rack";
|
||||
apiDoc["is_forced_entry"] = doc["is_forced_entry"] | false;
|
||||
String apiBody;
|
||||
serializeJson(apiDoc, apiBody);
|
||||
httpPost("/door-access/data", apiBody);
|
||||
|
||||
// Build payload untuk MQTT
|
||||
StaticJsonDocument<256> mqttDoc;
|
||||
mqttDoc["device_id"] = DEVICE_ID;
|
||||
mqttDoc["node"] = node;
|
||||
mqttDoc["type"] = type;
|
||||
mqttDoc["door"] = doc["door_opened"] | false;
|
||||
String mqttBody;
|
||||
serializeJson(mqttDoc, mqttBody);
|
||||
publishMQTT(TOPIC_REED, mqttBody);
|
||||
}
|
||||
|
||||
String type = packet.substring(0, sep1);
|
||||
String nodeId = packet.substring(sep1 + 1, sep2);
|
||||
String payload = packet.substring(sep2 + 1);
|
||||
// =========================================
|
||||
// VIBRATION
|
||||
// =========================================
|
||||
else if (type == "VIBRATION") {
|
||||
setAlert("VIBRATION detected");
|
||||
buzzerVib(); // bunyi di gateway
|
||||
|
||||
Serial.println("[LoRa RX] Type=" + type + " Node=" + nodeId);
|
||||
// Build payload untuk API — butuh x_axis, y_axis, z_axis
|
||||
// Jika node sender tidak kirim (firmware lama), generate simulasi di gateway
|
||||
StaticJsonDocument<256> apiDoc;
|
||||
apiDoc["device_id"] = DEVICE_ID;
|
||||
|
||||
checkWifi(); // Pastikan WiFi masih konek sebelum forward
|
||||
if (doc.containsKey("x_axis")) {
|
||||
apiDoc["x_axis"] = doc["x_axis"] | 0.0;
|
||||
apiDoc["y_axis"] = doc["y_axis"] | 0.0;
|
||||
apiDoc["z_axis"] = doc["z_axis"] | 0.0;
|
||||
} else {
|
||||
// Firmware lama tidak kirim axis — simulasikan nilai getaran terdeteksi
|
||||
apiDoc["x_axis"] = (float)(random(-300, 300)) / 100.0;
|
||||
apiDoc["y_axis"] = (float)(random(-300, 300)) / 100.0;
|
||||
apiDoc["z_axis"] = 2.0 + (float)(random(0, 150)) / 100.0;
|
||||
}
|
||||
apiDoc["threshold"] = doc["threshold"] | 2.0;
|
||||
|
||||
String apiBody;
|
||||
serializeJson(apiDoc, apiBody);
|
||||
httpPost("/vibration/data", apiBody);
|
||||
|
||||
// Build payload untuk MQTT
|
||||
StaticJsonDocument<256> mqttDoc;
|
||||
mqttDoc["device_id"] = DEVICE_ID;
|
||||
mqttDoc["node"] = node;
|
||||
mqttDoc["type"] = type;
|
||||
mqttDoc["x_axis"] = apiDoc["x_axis"];
|
||||
mqttDoc["y_axis"] = apiDoc["y_axis"];
|
||||
mqttDoc["z_axis"] = apiDoc["z_axis"];
|
||||
String mqttBody;
|
||||
serializeJson(mqttDoc, mqttBody);
|
||||
publishMQTT(TOPIC_VIBRATION, mqttBody);
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// STATUS (heartbeat)
|
||||
// =========================================
|
||||
else if (type == "STATUS") {
|
||||
// Kirim ke /lora/receive agar device tetap Online di dashboard
|
||||
StaticJsonDocument<128> apiDoc;
|
||||
apiDoc["node_id"] = node;
|
||||
apiDoc["gateway_id"] = gateway;
|
||||
apiDoc["payload"] = "HEARTBEAT|" + node;
|
||||
apiDoc["uptime"] = doc["uptime"] | 0;
|
||||
String apiBody;
|
||||
serializeJson(apiDoc, apiBody);
|
||||
httpPost("/lora/receive", apiBody);
|
||||
|
||||
// MQTT
|
||||
StaticJsonDocument<128> mqttDoc;
|
||||
mqttDoc["device_id"] = DEVICE_ID;
|
||||
mqttDoc["node"] = node;
|
||||
mqttDoc["type"] = type;
|
||||
mqttDoc["uptime"] = doc["uptime"] | 0;
|
||||
String mqttBody;
|
||||
serializeJson(mqttDoc, mqttBody);
|
||||
publishMQTT(TOPIC_STATUS, mqttBody);
|
||||
|
||||
Serial.println("[STATUS] RECEIVED & FORWARDED");
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// TEST
|
||||
// =========================================
|
||||
else if (type == "TEST") {
|
||||
Serial.println("[TEST] RECEIVED OK");
|
||||
StaticJsonDocument<128> mqttDoc;
|
||||
mqttDoc["device_id"] = DEVICE_ID;
|
||||
mqttDoc["node"] = node;
|
||||
mqttDoc["type"] = type;
|
||||
String mqttBody;
|
||||
serializeJson(mqttDoc, mqttBody);
|
||||
publishMQTT(TOPIC_TEST, mqttBody);
|
||||
}
|
||||
|
||||
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);
|
||||
Serial.println("[ROUTER] UNKNOWN TYPE: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -275,69 +409,113 @@ void prosesPacket(String packet, int rssi, float snr) {
|
|||
// ============================================
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
delay(500);
|
||||
Serial.println("\n==========================");
|
||||
Serial.println(" SMART RACK GATEWAY ");
|
||||
Serial.println("==========================");
|
||||
|
||||
pinMode(LED_WIFI, OUTPUT);
|
||||
pinMode(LED_LORA, OUTPUT);
|
||||
digitalWrite(LED_WIFI, LOW);
|
||||
digitalWrite(LED_LORA, LOW);
|
||||
pinMode(LED_WIFI, OUTPUT);
|
||||
pinMode(LED_LORA, OUTPUT);
|
||||
pinMode(LED_HIJAU, OUTPUT);
|
||||
pinMode(LED_MERAH, OUTPUT);
|
||||
pinMode(PIN_BUZZER, OUTPUT);
|
||||
digitalWrite(LED_WIFI, LOW);
|
||||
digitalWrite(LED_LORA, LOW);
|
||||
digitalWrite(LED_HIJAU, HIGH);
|
||||
digitalWrite(LED_MERAH, LOW);
|
||||
digitalWrite(PIN_BUZZER, 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();
|
||||
connectMQTT();
|
||||
|
||||
// LoRa — HARUS setting sama persis dengan Node Sender
|
||||
// =========================================
|
||||
// LORA — hardware reset + retry
|
||||
// =========================================
|
||||
Serial.println("[1] LoRa setPins");
|
||||
pinMode(LORA_RST, OUTPUT);
|
||||
digitalWrite(LORA_RST, LOW); delay(20);
|
||||
digitalWrite(LORA_RST, HIGH); delay(150);
|
||||
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);
|
||||
|
||||
Serial.println("[2] Starting LoRa...");
|
||||
int loraRetry = 0;
|
||||
while (!LoRa.begin(LORA_FREQ)) {
|
||||
loraRetry++;
|
||||
Serial.print("[LoRa] INIT FAILED, retry: ");
|
||||
Serial.println(loraRetry);
|
||||
digitalWrite(LORA_RST, LOW); delay(20);
|
||||
digitalWrite(LORA_RST, HIGH); delay(150);
|
||||
delay(500);
|
||||
if (loraRetry >= 10) {
|
||||
Serial.println("[LoRa] GIVING UP - RESTART");
|
||||
delay(1000);
|
||||
ESP.restart();
|
||||
}
|
||||
}
|
||||
|
||||
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!");
|
||||
LoRa.setSpreadingFactor(LORA_SF);
|
||||
LoRa.setSignalBandwidth(LORA_BW);
|
||||
LoRa.setCodingRate4(LORA_CR);
|
||||
LoRa.setSyncWord(LORA_SYNC_WORD);
|
||||
Serial.println("[3] LoRa READY");
|
||||
Serial.println("[SYSTEM] READY — menunggu paket...");
|
||||
Serial.println(" API : " + String(API_BASE));
|
||||
Serial.println(" DeviceID: " + String(DEVICE_ID));
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LOOP
|
||||
// ============================================
|
||||
void loop() {
|
||||
// Cek apakah ada paket LoRa masuk
|
||||
int packetSize = LoRa.parsePacket();
|
||||
checkWifi();
|
||||
if (!mqtt.connected()) connectMQTT();
|
||||
mqtt.loop();
|
||||
|
||||
// =========================================
|
||||
// RECEIVE LORA
|
||||
// =========================================
|
||||
int packetSize = LoRa.parsePacket();
|
||||
if (packetSize) {
|
||||
totalReceived++;
|
||||
blinkLoRa();
|
||||
|
||||
if (packetSize > 0) {
|
||||
String packet = "";
|
||||
while (LoRa.available()) {
|
||||
packet += (char)LoRa.read();
|
||||
}
|
||||
int rssi = LoRa.packetRssi();
|
||||
float snr = LoRa.packetSnr();
|
||||
|
||||
prosesPacket(packet, rssi, snr);
|
||||
Serial.println("\n========== LORA RX ==========");
|
||||
Serial.println(packet);
|
||||
Serial.print("RSSI: "); Serial.println(LoRa.packetRssi());
|
||||
Serial.print("SNR: "); Serial.println(LoRa.packetSnr());
|
||||
Serial.println("=============================");
|
||||
|
||||
routePacket(packet);
|
||||
}
|
||||
|
||||
// Print statistik setiap 60 detik
|
||||
// =========================================
|
||||
// ALERT RESET
|
||||
// =========================================
|
||||
if (alertActive && millis() - alertStartTime > ALERT_HOLD) {
|
||||
alertActive = false;
|
||||
digitalWrite(LED_HIJAU, HIGH);
|
||||
digitalWrite(LED_MERAH, LOW);
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// STATUS PRINT 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("[STATUS] Uptime : " + String(millis() / 1000) + "s");
|
||||
Serial.println("[STATUS] Diterima: " + String(totalReceived));
|
||||
Serial.println("[STATUS] API OK : " + String(totalSent));
|
||||
Serial.println("[STATUS] API FAIL: " + String(totalFailed));
|
||||
Serial.println("[STATUS] WiFi : " + String(WiFi.status() == WL_CONNECTED ? "OK" : "PUTUS"));
|
||||
Serial.println("[STATUS] MQTT : " + String(mqtt.connected() ? "OK" : "PUTUS"));
|
||||
Serial.println("─────────────────────────────────");
|
||||
lastStatusPrint = millis();
|
||||
}
|
||||
|
||||
delay(10); // Kecil saja agar LoRa tidak miss paket
|
||||
delay(10);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,182 +1,146 @@
|
|||
/**
|
||||
* ============================================
|
||||
* 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>
|
||||
#include <ArduinoJson.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
|
||||
// LORA PIN
|
||||
// ============================================
|
||||
#define LORA_SS 27
|
||||
#define LORA_RST 14
|
||||
#define LORA_DIO0 26
|
||||
#define LORA_FREQ 433E6 // 433 MHz — sesuaikan modul kamu
|
||||
#define LORA_FREQ 433E6
|
||||
|
||||
// ============================================
|
||||
// VARIABEL STATE SENSOR
|
||||
// LORA CONFIG
|
||||
// ============================================
|
||||
int pirState = LOW;
|
||||
int reedState = LOW;
|
||||
int vibrationState = LOW;
|
||||
int lastVibration = LOW;
|
||||
#define LORA_SYNC_WORD 0xA5
|
||||
#define LORA_SF 9
|
||||
#define LORA_BW 125E3
|
||||
#define LORA_CR 5
|
||||
|
||||
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;
|
||||
// ============================================
|
||||
// SENSOR PIN
|
||||
// ============================================
|
||||
#define PIN_SW420 34
|
||||
#define PIN_REED 32
|
||||
#define PIN_PIR 33
|
||||
#define PIN_BUZZER 25
|
||||
|
||||
// ============================================
|
||||
// LED
|
||||
// ============================================
|
||||
void setLED(bool bahaya) {
|
||||
digitalWrite(LED_MERAH, bahaya ? HIGH : LOW);
|
||||
digitalWrite(LED_HIJAU, bahaya ? LOW : HIGH);
|
||||
}
|
||||
#define LED_STATUS 2
|
||||
|
||||
// ============================================
|
||||
// BUZZER
|
||||
// NODE INFO
|
||||
// ============================================
|
||||
#define NODE_ID "NODE_001"
|
||||
#define GATEWAY_ID "GATEWAY_001"
|
||||
#define DEVICE_ID 1
|
||||
|
||||
// ============================================
|
||||
// TIMING
|
||||
// ============================================
|
||||
#define HEARTBEAT_INTERVAL 30000
|
||||
#define SENSOR_READ_INTERVAL 50
|
||||
#define PIR_DEBOUNCE 500 // ms — jeda minimum antar perubahan state
|
||||
#define PIR_COOLDOWN 10000 // ms — jeda minimum antar 2 deteksi berturut-turut
|
||||
|
||||
// ============================================
|
||||
// VIBRATION
|
||||
// ============================================
|
||||
#define VIB_DEBOUNCE 1500
|
||||
|
||||
// ============================================
|
||||
// BUZZER LANGSUNG (blocking) — dipanggil saat
|
||||
// deteksi agar pasti bunyi sebelum kirim LoRa
|
||||
// ============================================
|
||||
// PIR : 3x beep pendek cepat
|
||||
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);
|
||||
digitalWrite(PIN_BUZZER, HIGH); delay(80);
|
||||
digitalWrite(PIN_BUZZER, LOW); delay(80);
|
||||
}
|
||||
}
|
||||
|
||||
void buzzerStartup() {
|
||||
// REED : 1x beep panjang
|
||||
void buzzerReed() {
|
||||
digitalWrite(PIN_BUZZER, HIGH); delay(600);
|
||||
digitalWrite(PIN_BUZZER, LOW);
|
||||
}
|
||||
// VIB : 2x beep sedang
|
||||
void buzzerVib() {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
digitalWrite(BUZZER, HIGH); delay(200);
|
||||
digitalWrite(BUZZER, LOW); delay(100);
|
||||
digitalWrite(PIN_BUZZER, HIGH); delay(150);
|
||||
digitalWrite(PIN_BUZZER, LOW); delay(100);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// KIRIM PAKET LORA
|
||||
// Format: TYPE|NODE_ID|json_payload
|
||||
// PIR STATE
|
||||
// ============================================
|
||||
bool loRaSend(String type, String jsonPayload) {
|
||||
String packet = type + "|" + NODE_ID + "|" + jsonPayload;
|
||||
bool pirStableState = false;
|
||||
unsigned long pirLastChange = 0;
|
||||
unsigned long pirTriggerTime = 0;
|
||||
unsigned long pirLastDetect = 0; // waktu terakhir kirim DETECTED
|
||||
|
||||
LoRa.beginPacket();
|
||||
LoRa.print(packet);
|
||||
int result = LoRa.endPacket();
|
||||
// ============================================
|
||||
// REED STATE
|
||||
// ============================================
|
||||
bool reedWasOpen = false;
|
||||
unsigned long reedOpenTime = 0;
|
||||
|
||||
if (result) {
|
||||
Serial.println("[LoRa TX] ✓ " + packet.substring(0, 80));
|
||||
} else {
|
||||
Serial.println("[LoRa TX] ✗ Gagal kirim paket");
|
||||
}
|
||||
return result;
|
||||
// ============================================
|
||||
// VIBRATION STATE
|
||||
// ============================================
|
||||
bool vibLastState = LOW;
|
||||
unsigned long vibLastTrigger = 0;
|
||||
|
||||
// ============================================
|
||||
// TIMING
|
||||
// ============================================
|
||||
unsigned long lastHeartbeat = 0;
|
||||
unsigned long lastSensorRead = 0;
|
||||
|
||||
// ============================================
|
||||
// STATISTIC
|
||||
// ============================================
|
||||
unsigned long totalSent = 0;
|
||||
unsigned long totalFailed = 0;
|
||||
|
||||
// ============================================
|
||||
// PROTOTYPE
|
||||
// ============================================
|
||||
void readPIR(unsigned long now);
|
||||
void readReed(unsigned long now);
|
||||
void readVibration(unsigned long now);
|
||||
void buzzerPIR();
|
||||
void buzzerReed();
|
||||
void buzzerVib();
|
||||
void sendPIRData(bool motion, int intensity, int duration, String zone);
|
||||
void sendReedData(bool opened, int duration, bool forced);
|
||||
void sendVibrationData();
|
||||
void sendStatus();
|
||||
void sendLoRa(StaticJsonDocument<256>& doc);
|
||||
void sendLoRa(StaticJsonDocument<128>& doc);
|
||||
void blinkLED(int delayMs);
|
||||
void beep(int ms);
|
||||
|
||||
// ============================================
|
||||
// BLINK LED
|
||||
// ============================================
|
||||
void blinkLED(int delayMs = 50) {
|
||||
digitalWrite(LED_STATUS, HIGH);
|
||||
delay(delayMs);
|
||||
digitalWrite(LED_STATUS, LOW);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// KIRIM DATA PIR
|
||||
// BEEP
|
||||
// ============================================
|
||||
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);
|
||||
void beep(int ms) {
|
||||
digitalWrite(PIN_BUZZER, HIGH);
|
||||
delay(ms);
|
||||
digitalWrite(PIN_BUZZER, LOW);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
|
@ -184,121 +148,255 @@ void kirimHeartbeat() {
|
|||
// ============================================
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
delay(500);
|
||||
Serial.println("\n==========================");
|
||||
Serial.println(" SMART RACK NODE ");
|
||||
Serial.println("==========================");
|
||||
|
||||
// Pin sensor
|
||||
pinMode(PIR_PIN, INPUT);
|
||||
pinMode(REED_PIN, INPUT_PULLUP);
|
||||
pinMode(SW420_PIN, INPUT);
|
||||
pinMode(PIN_SW420, INPUT_PULLUP);
|
||||
pinMode(PIN_REED, INPUT_PULLUP);
|
||||
pinMode(PIN_PIR, INPUT);
|
||||
pinMode(PIN_BUZZER, OUTPUT);
|
||||
pinMode(LED_STATUS, OUTPUT);
|
||||
digitalWrite(PIN_BUZZER, LOW);
|
||||
digitalWrite(LED_STATUS, LOW);
|
||||
|
||||
// Pin output
|
||||
pinMode(BUZZER, OUTPUT);
|
||||
pinMode(LED_MERAH, OUTPUT);
|
||||
pinMode(LED_HIJAU, OUTPUT);
|
||||
// Startup beep
|
||||
beep(80); delay(80); beep(80);
|
||||
|
||||
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
|
||||
// =========================================
|
||||
Serial.println("[1] LoRa setPins");
|
||||
LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0);
|
||||
|
||||
Serial.println("[2] Starting LoRa...");
|
||||
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);
|
||||
Serial.println("[LoRa] INIT FAILED");
|
||||
while (1) {
|
||||
digitalWrite(PIN_BUZZER, HIGH);
|
||||
digitalWrite(LED_STATUS, HIGH);
|
||||
delay(100);
|
||||
digitalWrite(PIN_BUZZER, LOW);
|
||||
digitalWrite(LED_STATUS, LOW);
|
||||
delay(100);
|
||||
}
|
||||
}
|
||||
|
||||
LoRa.setTxPower(17);
|
||||
LoRa.setSpreadingFactor(9); // SF9 — balance range vs speed
|
||||
LoRa.setSignalBandwidth(125E3);
|
||||
LoRa.setCodingRate4(5);
|
||||
LoRa.setSpreadingFactor(LORA_SF);
|
||||
LoRa.setSignalBandwidth(LORA_BW);
|
||||
LoRa.setCodingRate4(LORA_CR);
|
||||
LoRa.setSyncWord(LORA_SYNC_WORD);
|
||||
Serial.println("[3] LoRa READY");
|
||||
|
||||
Serial.println("[LoRa] OK — " + String(LORA_FREQ / 1E6) + " MHz");
|
||||
// PIR stabilization — tunggu 30 detik agar sensor warm-up
|
||||
Serial.println("[PIR] Stabilizing 30s...");
|
||||
delay(30000);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
blinkLED(100);
|
||||
delay(100);
|
||||
}
|
||||
|
||||
buzzerStartup();
|
||||
Serial.println("Node siap!");
|
||||
sendStatus();
|
||||
lastHeartbeat = millis();
|
||||
Serial.println("[NODE] STARTED");
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LOOP
|
||||
// ============================================
|
||||
void loop() {
|
||||
pirState = digitalRead(PIR_PIN);
|
||||
reedState = digitalRead(REED_PIN);
|
||||
vibrationState = digitalRead(SW420_PIN);
|
||||
unsigned long now = millis();
|
||||
|
||||
// =========================================
|
||||
// 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");
|
||||
if (now - lastSensorRead >= SENSOR_READ_INTERVAL) {
|
||||
lastSensorRead = now;
|
||||
readPIR(now);
|
||||
readReed(now);
|
||||
readVibration(now);
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// REED SWITCH — Status Rak
|
||||
// =========================================
|
||||
if (reedState == HIGH && !doorOpen) {
|
||||
doorOpen = true;
|
||||
doorOpenTime = millis();
|
||||
Serial.println(">>> REED: Rak terbuka!");
|
||||
buzzerREED();
|
||||
setLED(true);
|
||||
if (now - lastHeartbeat >= HEARTBEAT_INTERVAL) {
|
||||
sendStatus();
|
||||
lastHeartbeat = now;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PIR
|
||||
// ============================================
|
||||
void readPIR(unsigned long now) {
|
||||
bool current = digitalRead(PIN_PIR);
|
||||
|
||||
// Debounce — abaikan perubahan dalam 500ms terakhir
|
||||
if (current != pirStableState && now - pirLastChange > PIR_DEBOUNCE) {
|
||||
pirLastChange = now;
|
||||
pirStableState = current;
|
||||
|
||||
if (pirStableState) {
|
||||
// Cooldown — jangan trigger lagi dalam 10 detik setelah deteksi terakhir
|
||||
if (now - pirLastDetect < PIR_COOLDOWN) {
|
||||
Serial.println("[PIR] Cooldown aktif, skip");
|
||||
return;
|
||||
}
|
||||
pirTriggerTime = now;
|
||||
pirLastDetect = now;
|
||||
Serial.println("[PIR] DETECTED");
|
||||
buzzerPIR();
|
||||
sendPIRData(true, 75, 0, "center");
|
||||
} else {
|
||||
int duration = (now - pirTriggerTime) / 1000;
|
||||
Serial.println("[PIR] END — durasi: " + String(duration) + "s");
|
||||
sendPIRData(false, 0, duration, "center");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// REED
|
||||
// ============================================
|
||||
void readReed(unsigned long now) {
|
||||
static bool lastStable = LOW;
|
||||
static unsigned long lastDebounce = 0;
|
||||
|
||||
bool current = digitalRead(PIN_REED);
|
||||
if (current != lastStable) {
|
||||
if (now - lastDebounce > 150) {
|
||||
lastDebounce = now;
|
||||
|
||||
if (current == HIGH) {
|
||||
reedOpenTime = now;
|
||||
reedWasOpen = true;
|
||||
Serial.println("[REED] OPEN");
|
||||
buzzerReed(); // bunyi langsung
|
||||
sendReedData(true, 0, false);
|
||||
} else {
|
||||
int duration = reedWasOpen ? (now - reedOpenTime) / 1000 : 0;
|
||||
reedWasOpen = false;
|
||||
Serial.println("[REED] CLOSE");
|
||||
buzzerReed(); // bunyi langsung
|
||||
sendReedData(false, duration, false);
|
||||
}
|
||||
lastStable = current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// VIBRATION
|
||||
// ============================================
|
||||
void readVibration(unsigned long now) {
|
||||
bool current = digitalRead(PIN_SW420);
|
||||
|
||||
if (current == HIGH && vibLastState == LOW) {
|
||||
if (now - vibLastTrigger > VIB_DEBOUNCE) {
|
||||
vibLastTrigger = now;
|
||||
Serial.println("[VIB] DETECTED");
|
||||
buzzerVib(); // bunyi langsung
|
||||
sendVibrationData();
|
||||
}
|
||||
}
|
||||
vibLastState = current;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SEND PIR
|
||||
// ============================================
|
||||
void sendPIRData(bool motion, int intensity, int duration, String zone) {
|
||||
StaticJsonDocument<256> doc;
|
||||
doc["type"] = "PIR";
|
||||
doc["node_id"] = NODE_ID;
|
||||
doc["gateway_id"] = GATEWAY_ID;
|
||||
doc["device_id"] = DEVICE_ID;
|
||||
doc["motion_detected"] = motion;
|
||||
doc["motion_intensity"] = intensity;
|
||||
doc["duration_seconds"] = duration;
|
||||
doc["detection_zone"] = zone;
|
||||
sendLoRa(doc);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SEND REED
|
||||
// ============================================
|
||||
void sendReedData(bool opened, int duration, bool forced) {
|
||||
StaticJsonDocument<256> doc;
|
||||
doc["type"] = "REED";
|
||||
doc["node_id"] = NODE_ID;
|
||||
doc["gateway_id"] = GATEWAY_ID;
|
||||
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"] = forced;
|
||||
sendLoRa(doc);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SEND VIBRATION
|
||||
// SW-420 = sensor digital (on/off), tidak ada sumbu XYZ fisik.
|
||||
// Nilai X/Y/Z disimulasikan agar API dapat hitung magnitude.
|
||||
// ============================================
|
||||
void sendVibrationData() {
|
||||
StaticJsonDocument<256> doc;
|
||||
doc["type"] = "VIBRATION";
|
||||
doc["node_id"] = NODE_ID;
|
||||
doc["gateway_id"] = GATEWAY_ID;
|
||||
doc["device_id"] = DEVICE_ID;
|
||||
doc["x_axis"] = random(-300, 300) / 100.0;
|
||||
doc["y_axis"] = random(-300, 300) / 100.0;
|
||||
doc["z_axis"] = 2.0 + random(0, 150) / 100.0; // Z selalu di atas threshold
|
||||
doc["threshold"] = 2.0;
|
||||
sendLoRa(doc);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SEND STATUS (heartbeat)
|
||||
// ============================================
|
||||
void sendStatus() {
|
||||
StaticJsonDocument<128> doc;
|
||||
doc["type"] = "STATUS";
|
||||
doc["node_id"] = NODE_ID;
|
||||
doc["gateway_id"] = GATEWAY_ID;
|
||||
doc["payload"] = "gateway hidup";
|
||||
doc["uptime"] = millis() / 1000;
|
||||
sendLoRa(doc);
|
||||
Serial.println("[STATUS] SENT");
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SEND LORA — 256 byte doc
|
||||
// ============================================
|
||||
void sendLoRa(StaticJsonDocument<256>& doc) {
|
||||
String payload;
|
||||
serializeJson(doc, payload);
|
||||
Serial.println("[LoRa TX] -> " + payload);
|
||||
LoRa.beginPacket();
|
||||
LoRa.print(payload);
|
||||
int result = LoRa.endPacket();
|
||||
if (result) {
|
||||
totalSent++;
|
||||
blinkLED(50);
|
||||
Serial.println("[LoRa] SUCCESS");
|
||||
} else {
|
||||
totalFailed++;
|
||||
Serial.println("[LoRa] FAILED");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SEND LORA — 128 byte doc
|
||||
// ============================================
|
||||
void sendLoRa(StaticJsonDocument<128>& doc) {
|
||||
String payload;
|
||||
serializeJson(doc, payload);
|
||||
Serial.println("[LoRa TX] -> " + payload);
|
||||
LoRa.beginPacket();
|
||||
LoRa.print(payload);
|
||||
int result = LoRa.endPacket();
|
||||
if (result) {
|
||||
totalSent++;
|
||||
blinkLED(50);
|
||||
} else {
|
||||
totalFailed++;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@
|
|||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
'timezone' => 'Asia/Jakarta',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -686,22 +686,8 @@ function toggleSidebar() {
|
|||
}
|
||||
}
|
||||
|
||||
// Auto refresh halaman setiap 30 detik untuk update alert
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 30000);
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// =============================================
|
||||
// BUZZER & LED REALTIME POLLING
|
||||
// =============================================
|
||||
|
||||
let alertDismissed = false;
|
||||
let pollingInterval = null;
|
||||
|
||||
// Threshold waktu deteksi dianggap "aktif" (dalam detik)
|
||||
const DETECTION_WINDOW = 30;
|
||||
const DETECTION_WINDOW = 60;
|
||||
|
||||
async function fetchSensorStatus() {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@
|
|||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 text-xs md:text-sm">Gerakan (24 jam)</p>
|
||||
<p class="text-2xl md:text-3xl font-display font-bold text-indigo-600">{{ $pirCount24h }}</p>
|
||||
<p id="stat-pir" class="text-2xl md:text-3xl font-display font-bold text-indigo-600">{{ $pirCount24h }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl shadow p-4 md:p-6 border border-gray-100 flex items-center gap-3 md:gap-4">
|
||||
|
|
@ -158,7 +158,7 @@
|
|||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 text-xs md:text-sm">Getaran (24 jam)</p>
|
||||
<p class="text-2xl md:text-3xl font-display font-bold text-yellow-600">{{ $vibrationCount24h }}</p>
|
||||
<p id="stat-vib" class="text-2xl md:text-3xl font-display font-bold text-yellow-600">{{ $vibrationCount24h }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl shadow p-4 md:p-6 border border-gray-100 flex items-center gap-3 md:gap-4">
|
||||
|
|
@ -167,7 +167,7 @@
|
|||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 text-xs md:text-sm">Rak Dibuka (24 jam)</p>
|
||||
<p class="text-2xl md:text-3xl font-display font-bold text-purple-600">{{ $reedCount24h }}</p>
|
||||
<p id="stat-reed" class="text-2xl md:text-3xl font-display font-bold text-purple-600">{{ $reedCount24h }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -241,7 +241,7 @@
|
|||
<p id="vib-status-text" class="text-xl md:text-3xl font-display font-bold {{ $vibrationLatest->is_abnormal ? 'text-red-600' : 'text-green-600' }} mb-1">
|
||||
{{ $vibrationLatest->is_abnormal ? '⚠ Abnormal' : '✓ Stabil' }}
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs md:text-sm">
|
||||
<p id="vib-detail" class="text-gray-500 text-xs md:text-sm">
|
||||
Magnitude: <span class="font-semibold text-gray-700">{{ number_format($vibrationLatest->magnitude ?? 0, 2) }}</span>
|
||||
· Status: <span class="font-semibold text-gray-700">{{ ucfirst($vibrationLatest->status ?? '-') }}</span>
|
||||
</p>
|
||||
|
|
@ -283,7 +283,7 @@
|
|||
<p id="reed-status-text" class="text-xl md:text-3xl font-display font-bold {{ $reedOpen ? 'text-red-600' : 'text-green-600' }} mb-1">
|
||||
{{ $reedOpen ? '⚠ Terbuka' : '✓ Tertutup' }}
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs md:text-sm">
|
||||
<p id="reed-detail" class="text-gray-500 text-xs md:text-sm">
|
||||
Tipe: <span class="font-semibold text-gray-700">{{ ucfirst($reedLatest->access_type ?? $reedLatest->access_level ?? 'manual') }}</span>
|
||||
· Durasi: <span class="font-semibold text-gray-700">{{ $reedLatest->duration_seconds ?? $reedLatest->open_duration_seconds ?? 0 }}s</span>
|
||||
</p>
|
||||
|
|
@ -312,7 +312,7 @@
|
|||
</div>
|
||||
<h4 class="font-display font-bold text-gray-800 text-sm sm:text-base">Riwayat PIR</h4>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div id="pir-history" class="space-y-3">
|
||||
@forelse($pirHistory as $pir)
|
||||
<div class="flex items-center justify-between p-3 rounded-xl {{ $pir->motion_detected ? 'bg-red-50 border border-red-100' : 'bg-gray-50 border border-gray-100' }}">
|
||||
<div>
|
||||
|
|
@ -338,7 +338,7 @@
|
|||
</div>
|
||||
<h4 class="font-display font-bold text-gray-800 text-sm sm:text-base">Riwayat Getaran</h4>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div id="vib-history" class="space-y-3">
|
||||
@forelse($vibrationHistory as $vib)
|
||||
<div class="flex items-center justify-between p-3 rounded-xl {{ $vib->is_abnormal ? 'bg-red-50 border border-red-100' : 'bg-gray-50 border border-gray-100' }}">
|
||||
<div>
|
||||
|
|
@ -363,7 +363,7 @@
|
|||
</div>
|
||||
<h4 class="font-display font-bold text-gray-800 text-sm sm:text-base">Riwayat Reed Switch</h4>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div id="reed-history" class="space-y-3">
|
||||
@forelse($reedHistory as $reed)
|
||||
@php $reedOpen = $reed->door_opened ?? $reed->door_open ?? false; @endphp
|
||||
<div class="flex items-center justify-between p-3 rounded-xl {{ $reedOpen ? 'bg-red-50 border border-red-100' : 'bg-gray-50 border border-gray-100' }}">
|
||||
|
|
@ -456,55 +456,208 @@ function toggleSidebar() {
|
|||
}
|
||||
|
||||
// =============================================
|
||||
// POLLING REALTIME — update kartu sensor tanpa reload halaman
|
||||
// HELPER — format tanggal lokal
|
||||
// =============================================
|
||||
const DETECTION_WINDOW = 30; // detik
|
||||
function formatDate(iso) {
|
||||
const d = new Date(iso);
|
||||
const months = ['Jan','Feb','Mar','Apr','Mei','Jun','Jul','Agu','Sep','Okt','Nov','Des'];
|
||||
const dd = String(d.getDate()).padStart(2, '0');
|
||||
const mon = months[d.getMonth()];
|
||||
const hh = String(d.getHours()).padStart(2, '0');
|
||||
const mm = String(d.getMinutes()).padStart(2, '0');
|
||||
const ss = String(d.getSeconds()).padStart(2, '0');
|
||||
return `${dd} ${mon} ${hh}:${mm}:${ss}`;
|
||||
}
|
||||
|
||||
function timeAgo(iso) {
|
||||
const sec = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
|
||||
if (sec < 10) return 'Baru saja';
|
||||
if (sec < 60) return sec + ' detik lalu';
|
||||
if (sec < 3600) return Math.floor(sec / 60) + ' menit lalu';
|
||||
return Math.floor(sec / 3600) + ' jam lalu';
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// FLASH efek — highlight baris paling atas
|
||||
// saat ada data baru
|
||||
// =============================================
|
||||
function flashNew(containerId) {
|
||||
const el = document.getElementById(containerId);
|
||||
if (!el) return;
|
||||
const first = el.firstElementChild;
|
||||
if (!first) return;
|
||||
first.style.transition = 'background-color 0.3s';
|
||||
first.style.backgroundColor = '#fef9c3'; // kuning muda
|
||||
setTimeout(() => { first.style.backgroundColor = ''; }, 1500);
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// RENDER RIWAYAT
|
||||
// =============================================
|
||||
function renderPirHistory(data) {
|
||||
const el = document.getElementById('pir-history');
|
||||
if (!el) return;
|
||||
if (!data || data.length === 0) {
|
||||
el.innerHTML = '<p class="text-gray-400 text-sm text-center py-4">Belum ada data</p>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = data.map(d => {
|
||||
const active = d.motion_detected;
|
||||
const bg = active ? 'bg-red-50 border border-red-100' : 'bg-gray-50 border border-gray-100';
|
||||
const txt = active ? 'text-red-700' : 'text-gray-700';
|
||||
const dot = active ? 'bg-red-500' : 'bg-green-500';
|
||||
const label = active ? `Gerakan — ${d.motion_intensity ?? 0}%` : 'Aman';
|
||||
return `<div class="flex items-center justify-between p-3 rounded-xl ${bg}">
|
||||
<div>
|
||||
<p class="text-sm font-semibold ${txt}">${label}</p>
|
||||
<p class="text-xs text-gray-400">${formatDate(d.recorded_at)}</p>
|
||||
</div>
|
||||
<div class="w-2 h-2 rounded-full ${dot}"></div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderVibHistory(data) {
|
||||
const el = document.getElementById('vib-history');
|
||||
if (!el) return;
|
||||
if (!data || data.length === 0) {
|
||||
el.innerHTML = '<p class="text-gray-400 text-sm text-center py-4">Belum ada data</p>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = data.map(d => {
|
||||
const active = d.is_abnormal;
|
||||
const bg = active ? 'bg-red-50 border border-red-100' : 'bg-gray-50 border border-gray-100';
|
||||
const txt = active ? 'text-red-700' : 'text-gray-700';
|
||||
const dot = active ? 'bg-red-500' : 'bg-green-500';
|
||||
const mag = parseFloat(d.magnitude ?? 0).toFixed(2);
|
||||
const st = (d.status ?? 'normal').charAt(0).toUpperCase() + (d.status ?? 'normal').slice(1);
|
||||
return `<div class="flex items-center justify-between p-3 rounded-xl ${bg}">
|
||||
<div>
|
||||
<p class="text-sm font-semibold ${txt}">${st} — ${mag}</p>
|
||||
<p class="text-xs text-gray-400">${formatDate(d.recorded_at)}</p>
|
||||
</div>
|
||||
<div class="w-2 h-2 rounded-full ${dot}"></div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderReedHistory(data) {
|
||||
const el = document.getElementById('reed-history');
|
||||
if (!el) return;
|
||||
if (!data || data.length === 0) {
|
||||
el.innerHTML = '<p class="text-gray-400 text-sm text-center py-4">Belum ada data</p>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = data.map(d => {
|
||||
const open = d.door_opened ?? d.door_open ?? false;
|
||||
const bg = open ? 'bg-red-50 border border-red-100' : 'bg-gray-50 border border-gray-100';
|
||||
const txt = open ? 'text-red-700' : 'text-gray-700';
|
||||
const dot = open ? 'bg-red-500' : 'bg-green-500';
|
||||
const type = d.access_type ?? d.access_level ?? 'manual';
|
||||
const label = (open ? 'Terbuka' : 'Tertutup') + ' — ' + type.charAt(0).toUpperCase() + type.slice(1);
|
||||
return `<div class="flex items-center justify-between p-3 rounded-xl ${bg}">
|
||||
<div>
|
||||
<p class="text-sm font-semibold ${txt}">${label}</p>
|
||||
<p class="text-xs text-gray-400">${formatDate(d.recorded_at)}</p>
|
||||
</div>
|
||||
<div class="w-2 h-2 rounded-full ${dot}"></div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// STATE — simpan ID data terakhir yang sudah
|
||||
// ditampilkan, untuk deteksi data baru
|
||||
// =============================================
|
||||
let lastPirId = 0;
|
||||
let lastVibId = 0;
|
||||
let lastReedId = 0;
|
||||
|
||||
// =============================================
|
||||
// POLLING REALTIME — 5 detik
|
||||
// =============================================
|
||||
async function pollSensorData() {
|
||||
try {
|
||||
const [pirRes, vibRes, reedRes] = await Promise.all([
|
||||
fetch('/api/pir/readings?limit=1').then(r => r.json()).catch(() => null),
|
||||
fetch('/api/vibration/readings?limit=1').then(r => r.json()).catch(() => null),
|
||||
fetch('/api/door-access/readings?limit=1').then(r => r.json()).catch(() => null),
|
||||
const [pirRes, vibRes, reedRes, pirStat24, vibStat24, reedStat24] = await Promise.all([
|
||||
fetch('/api/pir/readings?limit=5').then(r => r.json()).catch(() => null),
|
||||
fetch('/api/vibration/readings?limit=5').then(r => r.json()).catch(() => null),
|
||||
fetch('/api/door-access/readings?limit=5').then(r => r.json()).catch(() => null),
|
||||
fetch('/api/pir/statistics?hours=24').then(r => r.json()).catch(() => null),
|
||||
fetch('/api/vibration/statistics?hours=24').then(r => r.json()).catch(() => null),
|
||||
fetch('/api/door-access/statistics?hours=24').then(r => r.json()).catch(() => null),
|
||||
]);
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// PIR
|
||||
// ---- Update statistik 24 jam ----
|
||||
const pirStatEl = document.getElementById('stat-pir');
|
||||
const vibStatEl = document.getElementById('stat-vib');
|
||||
const reedStatEl = document.getElementById('stat-reed');
|
||||
if (pirStatEl && pirStat24?.success) pirStatEl.textContent = pirStat24.data?.motion_detected_count ?? pirStatEl.textContent;
|
||||
if (vibStatEl && vibStat24?.success) vibStatEl.textContent = vibStat24.data?.warning_count + (vibStat24.data?.critical_count ?? 0) ?? vibStatEl.textContent;
|
||||
if (reedStatEl && reedStat24?.success) reedStatEl.textContent = reedStat24.data?.door_opened_count ?? reedStatEl.textContent;
|
||||
|
||||
// ---- PIR ----
|
||||
if (pirRes?.success && pirRes.data?.length > 0) {
|
||||
const d = pirRes.data[0];
|
||||
const age = (now - new Date(d.recorded_at).getTime()) / 1000;
|
||||
const active = d.motion_detected && age <= DETECTION_WINDOW;
|
||||
const d = pirRes.data[0];
|
||||
const isNew = d.id > lastPirId;
|
||||
const age = (now - new Date(d.recorded_at).getTime()) / 1000;
|
||||
const active = d.motion_detected && age <= 60;
|
||||
|
||||
document.getElementById('pir-status-text').textContent = active ? '⚠ Gerakan' : '✓ Aman';
|
||||
document.getElementById('pir-status-text').className = 'text-xl md:text-3xl font-display font-bold mb-1 ' + (active ? 'text-red-600' : 'text-green-600');
|
||||
document.getElementById('pir-detail').textContent = 'Intensitas: ' + (d.motion_intensity ?? 0) + '% · Durasi: ' + (d.duration_seconds ?? 0) + 's';
|
||||
document.getElementById('pir-update').textContent = 'Baru saja';
|
||||
document.getElementById('pir-update').className = active ? 'text-red-600 font-semibold' : 'text-green-600 font-semibold';
|
||||
document.getElementById('pir-status-text').className = 'text-xl md:text-3xl font-display font-bold mb-1 ' + (active ? 'text-red-600' : 'text-green-600');
|
||||
document.getElementById('pir-detail').textContent = 'Intensitas: ' + (d.motion_intensity ?? 0) + '% · Durasi: ' + (d.duration_seconds ?? 0) + 's';
|
||||
document.getElementById('pir-update').textContent = timeAgo(d.recorded_at);
|
||||
document.getElementById('pir-update').className = active ? 'text-red-600 font-semibold text-xs md:text-sm' : 'text-green-600 font-semibold text-xs md:text-sm';
|
||||
|
||||
renderPirHistory(pirRes.data);
|
||||
if (isNew && lastPirId > 0) flashNew('pir-history');
|
||||
lastPirId = d.id;
|
||||
}
|
||||
|
||||
// Vibration
|
||||
// ---- Vibration ----
|
||||
if (vibRes?.success && vibRes.data?.length > 0) {
|
||||
const d = vibRes.data[0];
|
||||
const age = (now - new Date(d.recorded_at).getTime()) / 1000;
|
||||
const active = d.is_abnormal && age <= DETECTION_WINDOW;
|
||||
const d = vibRes.data[0];
|
||||
const isNew = d.id > lastVibId;
|
||||
const age = (now - new Date(d.recorded_at).getTime()) / 1000;
|
||||
const active = d.is_abnormal && age <= 30;
|
||||
|
||||
document.getElementById('vib-status-text').textContent = active ? '⚠ Abnormal' : '✓ Stabil';
|
||||
document.getElementById('vib-status-text').className = 'text-xl md:text-3xl font-display font-bold mb-1 ' + (active ? 'text-red-600' : 'text-green-600');
|
||||
document.getElementById('vib-update').textContent = 'Baru saja';
|
||||
document.getElementById('vib-update').className = active ? 'text-red-600 font-semibold' : 'text-green-600 font-semibold';
|
||||
document.getElementById('vib-status-text').className = 'text-xl md:text-3xl font-display font-bold mb-1 ' + (active ? 'text-red-600' : 'text-green-600');
|
||||
const mag = parseFloat(d.magnitude ?? 0).toFixed(2);
|
||||
const vibDetail = document.getElementById('vib-detail');
|
||||
if (vibDetail) vibDetail.textContent = 'Magnitude: ' + mag + ' · Status: ' + (d.status ?? '-');
|
||||
document.getElementById('vib-update').textContent = timeAgo(d.recorded_at);
|
||||
document.getElementById('vib-update').className = active ? 'text-red-600 font-semibold text-xs md:text-sm' : 'text-green-600 font-semibold text-xs md:text-sm';
|
||||
|
||||
renderVibHistory(vibRes.data);
|
||||
if (isNew && lastVibId > 0) flashNew('vib-history');
|
||||
lastVibId = d.id;
|
||||
}
|
||||
|
||||
// Reed Switch
|
||||
// ---- Reed Switch ----
|
||||
if (reedRes?.success && reedRes.data?.length > 0) {
|
||||
const d = reedRes.data[0];
|
||||
const age = (now - new Date(d.recorded_at).getTime()) / 1000;
|
||||
const active = (d.door_opened ?? d.door_open ?? false) && age <= DETECTION_WINDOW;
|
||||
const d = reedRes.data[0];
|
||||
const isNew = d.id > lastReedId;
|
||||
const age = (now - new Date(d.recorded_at).getTime()) / 1000;
|
||||
const active = (d.door_opened ?? d.door_open ?? false) && age <= 30;
|
||||
|
||||
document.getElementById('reed-status-text').textContent = active ? '⚠ Terbuka' : '✓ Tertutup';
|
||||
document.getElementById('reed-status-text').className = 'text-xl md:text-3xl font-display font-bold mb-1 ' + (active ? 'text-red-600' : 'text-green-600');
|
||||
document.getElementById('reed-update').textContent = 'Baru saja';
|
||||
document.getElementById('reed-update').className = active ? 'text-red-600 font-semibold' : 'text-green-600 font-semibold';
|
||||
document.getElementById('reed-status-text').className = 'text-xl md:text-3xl font-display font-bold mb-1 ' + (active ? 'text-red-600' : 'text-green-600');
|
||||
const reedDetail = document.getElementById('reed-detail');
|
||||
if (reedDetail) {
|
||||
const type = d.access_type ?? d.access_level ?? 'manual';
|
||||
reedDetail.textContent = 'Tipe: ' + type.charAt(0).toUpperCase() + type.slice(1) + ' · Durasi: ' + (d.duration_seconds ?? d.open_duration_seconds ?? 0) + 's';
|
||||
}
|
||||
document.getElementById('reed-update').textContent = timeAgo(d.recorded_at);
|
||||
document.getElementById('reed-update').className = active ? 'text-red-600 font-semibold text-xs md:text-sm' : 'text-green-600 font-semibold text-xs md:text-sm';
|
||||
|
||||
renderReedHistory(reedRes.data);
|
||||
if (isNew && lastReedId > 0) flashNew('reed-history');
|
||||
lastReedId = d.id;
|
||||
}
|
||||
|
||||
// Update dot indicator
|
||||
// Dot hijau = polling OK
|
||||
const dot = document.getElementById('polling-dot');
|
||||
dot.classList.remove('bg-red-500');
|
||||
dot.classList.add('bg-green-500');
|
||||
|
|
@ -517,8 +670,9 @@ function toggleSidebar() {
|
|||
}
|
||||
}
|
||||
|
||||
// Poll setiap 10 detik
|
||||
setInterval(pollSensorData, 10000);
|
||||
// Jalankan langsung, lalu tiap 5 detik
|
||||
pollSensorData();
|
||||
setInterval(pollSensorData, 5000);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"topic": "keamanan/pir",
|
||||
"payload": "{\"device_id\":1,\"node\":\"NODE_001\",\"type\":\"PIR\",\"motion\":true}"
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"device_id": 1,
|
||||
"motion_detected": true
|
||||
}
|
||||
Loading…
Reference in New Issue