TKK_E32231477/app/Services/LoRaProcessingService.php

598 lines
20 KiB
PHP

<?php
namespace App\Services;
use App\Models\LoRaMessage;
use App\Models\VibrationReading;
use App\Models\PirReading;
use App\Models\DoorAccessReading;
use App\Models\Device;
use App\Services\NotificationService;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;
class LoRaProcessingService
{
protected $notificationService;
public function __construct(NotificationService $notificationService)
{
$this->notificationService = $notificationService;
}
/**
* Process inbound LoRa message
*/
public function processInboundMessage(LoRaMessage $message): array
{
try {
// Parse payload
$parsedData = $message->parsePayload();
// Update message dengan parsed data
$message->update([
'parsed_data' => $parsedData
]);
$result = ['success' => false, 'action' => 'none'];
// Process berdasarkan message type
switch ($message->message_type) {
case 'sensor_data':
$result = $this->processSensorData($message, $parsedData);
break;
case 'heartbeat':
$result = $this->processHeartbeat($message, $parsedData);
break;
case 'ack':
$result = $this->processAcknowledgment($message, $parsedData);
break;
default:
$result = ['success' => true, 'action' => 'logged'];
}
// Mark message as processed
$message->markAsProcessed($result['success'], $result['error'] ?? null);
Log::info("LoRa message processed", [
'message_id' => $message->id,
'node_id' => $message->node_id,
'message_type' => $message->message_type,
'result' => $result
]);
return $result;
} catch (\Exception $e) {
$message->markAsProcessed(false, $e->getMessage());
Log::error("LoRa message processing failed", [
'message_id' => $message->id,
'error' => $e->getMessage()
]);
return ['success' => false, 'error' => $e->getMessage()];
}
}
/**
* Process sensor data dari LoRa message
*/
private function processSensorData(LoRaMessage $message, array $parsedData): array
{
if (!isset($parsedData['sensor_type'])) {
return ['success' => false, 'error' => 'Missing sensor type'];
}
$sensorType = strtoupper($parsedData['sensor_type']);
switch ($sensorType) {
case 'VIBRATION':
return $this->processVibrationData($message, $parsedData);
case 'PIR':
return $this->processPirData($message, $parsedData);
case 'DOOR':
return $this->processDoorAccessData($message, $parsedData);
default:
return ['success' => false, 'error' => 'Unknown sensor type: ' . $sensorType];
}
}
/**
* Process vibration sensor data
*/
private function processVibrationData(LoRaMessage $message, array $data): array
{
try {
// Normalisasi field — JSON ESP32 bisa kirim "x" atau "x_axis"
$xAxis = (float) ($data['x_axis'] ?? $data['x'] ?? 0);
$yAxis = (float) ($data['y_axis'] ?? $data['y'] ?? 0);
$zAxis = (float) ($data['z_axis'] ?? $data['z'] ?? 0);
$magnitude = sqrt(pow($xAxis, 2) + pow($yAxis, 2) + pow($zAxis, 2));
$threshold = (float) ($data['threshold'] ?? 2.0);
$isAbnormal = $magnitude > $threshold;
$status = 'normal';
if ($magnitude > $threshold * 1.5) {
$status = 'critical';
} elseif ($magnitude > $threshold) {
$status = 'warning';
}
// Resolve device_id
$deviceId = $message->device_id ?? Device::where('is_active', true)->first()?->id ?? Device::first()?->id;
if (!$deviceId) {
return ['success' => false, 'error' => 'No device found'];
}
// Simpan ke VibrationReading
$vibrationReading = VibrationReading::create([
'device_id' => $deviceId,
'x_axis' => $xAxis,
'y_axis' => $yAxis,
'z_axis' => $zAxis,
'magnitude' => $magnitude,
'is_abnormal' => $isAbnormal,
'threshold' => $threshold,
'status' => $status,
'metadata' => [
'lora_message_id' => $message->id,
'node_id' => $message->node_id,
'rssi' => $message->rssi,
'snr' => $message->snr,
'source' => 'lora',
],
'recorded_at' => $message->received_at ?? now(),
]);
Device::markOnline($deviceId);
return [
'success' => true,
'action' => 'vibration_data_saved',
'vibration_reading_id' => $vibrationReading->id,
'magnitude' => $magnitude,
'status' => $status,
'is_abnormal' => $isAbnormal,
];
} catch (\Exception $e) {
return ['success' => false, 'error' => 'Vibration processing failed: ' . $e->getMessage()];
}
}
/**
* Process PIR sensor data
*/
private function processPirData(LoRaMessage $message, array $data): array
{
try {
$isAuthorizedTime = $this->checkAuthorizedTime();
// Normalisasi field — JSON ESP32 bisa kirim "motion" atau "motion_detected"
$motionDetected = filter_var(
$data['motion_detected'] ?? $data['motion'] ?? false,
FILTER_VALIDATE_BOOLEAN
);
$isSuspicious = $this->determineSuspiciousPirMotion($data, $isAuthorizedTime);
$motionType = 'normal';
if (!$isAuthorizedTime) {
$motionType = 'unauthorized';
} elseif ($isSuspicious) {
$motionType = 'suspicious';
}
// Resolve device_id — gunakan dari message atau fallback
$deviceId = $message->device_id ?? Device::where('is_active', true)->first()?->id ?? Device::first()?->id;
if (!$deviceId) {
return ['success' => false, 'error' => 'No device found'];
}
// Simpan ke PirReading
$pirReading = PirReading::create([
'device_id' => $deviceId,
'motion_detected' => $motionDetected,
'motion_intensity' => (int) ($data['motion_intensity'] ?? ($motionDetected ? 50 : 0)),
'duration_seconds' => (int) ($data['duration_seconds'] ?? 0),
'is_authorized_time' => $isAuthorizedTime,
'is_suspicious' => $isSuspicious,
'motion_type' => $motionType,
'detection_zone' => $data['detection_zone'] ?? 'center',
'metadata' => [
'lora_message_id' => $message->id,
'node_id' => $message->node_id,
'rssi' => $message->rssi,
'snr' => $message->snr,
'source' => 'lora',
],
'recorded_at' => $message->received_at ?? now(),
]);
Device::markOnline($deviceId);
return [
'success' => true,
'action' => 'pir_data_saved',
'pir_reading_id' => $pirReading->id,
'motion_type' => $motionType,
'is_suspicious' => $isSuspicious,
];
} catch (\Exception $e) {
return ['success' => false, 'error' => 'PIR processing failed: ' . $e->getMessage()];
}
}
/**
* Process door access sensor data
*/
private function processDoorAccessData(LoRaMessage $message, array $data): array
{
try {
$isAuthorizedTime = $this->checkAuthorizedTime();
// Normalisasi field — JSON ESP32 bisa kirim "door", "door_opened", atau "door_open"
$doorOpened = filter_var(
$data['door_opened'] ?? $data['door'] ?? $data['door_open'] ?? false,
FILTER_VALIDATE_BOOLEAN
);
$isAuthorizedAccess = $this->determineAuthorizedDoorAccess($data, $isAuthorizedTime);
$isSuspicious = $this->determineSuspiciousDoorAccess($data, $isAuthorizedAccess);
$accessType = $this->determineDoorAccessType($data, $isAuthorizedAccess, $isAuthorizedTime);
// Resolve device_id
$deviceId = $message->device_id ?? Device::where('is_active', true)->first()?->id ?? Device::first()?->id;
if (!$deviceId) {
return ['success' => false, 'error' => 'No device found'];
}
// Simpan ke DoorAccessReading
$doorReading = DoorAccessReading::create([
'device_id' => $deviceId,
'door_opened' => $doorOpened,
'is_authorized_access'=> $isAuthorizedAccess,
'access_type' => $accessType,
'access_method' => $data['access_method'] ?? 'unknown',
'user_id_card' => $data['user_id_card'] ?? null,
'duration_seconds' => (int) ($data['duration_seconds'] ?? 0),
'is_suspicious' => $isSuspicious,
'door_location' => $data['door_location'] ?? 'main_entrance',
'is_forced_entry' => ($data['access_method'] ?? '') === 'force',
'metadata' => [
'lora_message_id' => $message->id,
'node_id' => $message->node_id,
'rssi' => $message->rssi,
'snr' => $message->snr,
'source' => 'lora',
],
'recorded_at' => $message->received_at ?? now(),
]);
Device::markOnline($deviceId);
return [
'success' => true,
'action' => 'door_access_data_saved',
'door_reading_id' => $doorReading->id,
'access_type' => $accessType,
'is_suspicious' => $isSuspicious,
];
} catch (\Exception $e) {
return ['success' => false, 'error' => 'Door access processing failed: ' . $e->getMessage()];
}
}
/**
* Process heartbeat message
*/
private function processHeartbeat(LoRaMessage $message, array $data): array
{
try {
// Update device last seen
if ($message->device) {
$message->device->update([
'last_seen' => $message->received_at ?? now(),
'status' => 'online'
]);
}
// Check battery level untuk alert
$batteryLevel = $data['battery_level'] ?? null;
if ($batteryLevel !== null && $batteryLevel < 20) {
$this->sendLowBatteryAlert($message, $batteryLevel);
}
return [
'success' => true,
'action' => 'heartbeat_processed',
'battery_level' => $batteryLevel,
'signal_strength' => $data['signal_strength'] ?? null,
'uptime' => $data['uptime_seconds'] ?? null
];
} catch (\Exception $e) {
return ['success' => false, 'error' => 'Heartbeat processing failed: ' . $e->getMessage()];
}
}
/**
* Process acknowledgment message
*/
private function processAcknowledgment(LoRaMessage $message, array $data): array
{
try {
$commandId = $data['command_id'] ?? null;
if ($commandId) {
// Cari outbound message yang sesuai
$outboundMessage = LoRaMessage::where('direction', 'outbound')
->where('metadata->command_id', $commandId)
->first();
if ($outboundMessage) {
$outboundMessage->markAsAcknowledged();
}
}
return [
'success' => true,
'action' => 'acknowledgment_processed',
'command_id' => $commandId,
'ack_status' => $data['ack_status'] ?? 'unknown'
];
} catch (\Exception $e) {
return ['success' => false, 'error' => 'ACK processing failed: ' . $e->getMessage()];
}
}
/**
* Transmit message ke LoRa gateway
*/
public function transmitMessage(LoRaMessage $message): array
{
try {
// Implementasi tergantung jenis LoRa gateway yang digunakan
// Contoh untuk HTTP-based gateway
$gatewayUrl = config('lora.gateway_url', 'http://localhost:8080/lora/send');
$payload = [
'node_id' => $message->node_id,
'payload' => $message->payload,
'spreading_factor' => $message->spreading_factor ?? 7,
'frequency' => $message->frequency ?? 868.1,
'tx_power' => 14
];
// Kirim ke gateway (simulasi)
Log::info("LoRa message would be transmitted", [
'gateway_url' => $gatewayUrl,
'payload' => $payload
]);
// TODO: Implementasi actual transmission
/*
$response = Http::timeout(10)->post($gatewayUrl, $payload);
if ($response->successful()) {
$message->update(['status' => 'transmitted']);
return ['success' => true, 'gateway_response' => $response->json()];
} else {
$message->update(['status' => 'failed']);
return ['success' => false, 'error' => 'Gateway transmission failed'];
}
*/
// Simulasi sukses
$message->update(['status' => 'transmitted']);
return ['success' => true, 'simulated' => true];
} catch (\Exception $e) {
$message->update(['status' => 'failed', 'error_message' => $e->getMessage()]);
Log::error("LoRa transmission failed", [
'message_id' => $message->id,
'error' => $e->getMessage()
]);
return ['success' => false, 'error' => $e->getMessage()];
}
}
/**
* Send low battery alert
*/
private function sendLowBatteryAlert(LoRaMessage $message, int $batteryLevel): void
{
try {
Log::warning("Low battery alert", [
'node_id' => $message->node_id,
'battery_level' => $batteryLevel,
'rssi' => $message->rssi
]);
// TODO: Implementasi notification untuk low battery
// $this->notificationService->sendLowBatteryAlert($message, $batteryLevel);
} catch (\Exception $e) {
Log::error("Failed to send low battery alert: " . $e->getMessage());
}
}
/**
* Check authorized time (jam kerja)
*/
private function checkAuthorizedTime(): bool
{
$hour = now()->hour;
return $hour >= 8 && $hour < 17;
}
/**
* Determine suspicious PIR motion
*/
private function determineSuspiciousPirMotion(array $data, bool $isAuthorizedTime): bool
{
if (!$isAuthorizedTime) {
return true;
}
$intensity = $data['motion_intensity'] ?? 0;
$duration = $data['duration_seconds'] ?? 0;
return $intensity > 80 || $duration > 300;
}
/**
* Determine authorized door access
*/
private function determineAuthorizedDoorAccess(array $data, bool $isAuthorizedTime): bool
{
$method = $data['access_method'] ?? 'unknown';
$idCard = $data['user_id_card'] ?? null;
if ($method === 'force') {
return false;
}
if ($method === 'emergency') {
return true;
}
if ($method === 'keycard' && $idCard && preg_match('/^EMP-\d{4}$/', $idCard)) {
return true;
}
return false;
}
/**
* Determine suspicious door access
*/
private function determineSuspiciousDoorAccess(array $data, bool $isAuthorizedAccess): bool
{
if (!$isAuthorizedAccess) {
return true;
}
$duration = $data['duration_seconds'] ?? 0;
return $duration > 180;
}
/**
* Determine door access type
*/
private function determineDoorAccessType(array $data, bool $isAuthorizedAccess, bool $isAuthorizedTime): string
{
$method = $data['access_method'] ?? 'unknown';
if ($method === 'force') {
return 'forced_entry';
}
if ($method === 'emergency') {
return 'emergency';
}
if ($method === 'maintenance') {
return 'maintenance';
}
if ($isAuthorizedAccess && $isAuthorizedTime) {
return 'authorized';
}
if (!$isAuthorizedTime) {
return 'after_hours';
}
return 'unauthorized';
}
/**
* Send command to LoRa node
*/
public function sendCommandToNode(string $nodeId, string $action, array $parameters = []): array
{
try {
$commandId = uniqid('CMD_');
$payload = LoRaMessage::generateCommandPayload($action, $parameters, $commandId);
$message = LoRaMessage::create([
'node_id' => $nodeId,
'gateway_id' => 'GATEWAY_001',
'direction' => 'outbound',
'message_type' => 'command',
'payload' => $payload,
'status' => 'pending',
'metadata' => [
'command_id' => $commandId,
'action' => $action,
'parameters' => $parameters
],
'transmitted_at' => now()
]);
$result = $this->transmitMessage($message);
return array_merge($result, [
'message_id' => $message->id,
'command_id' => $commandId
]);
} catch (\Exception $e) {
return ['success' => false, 'error' => $e->getMessage()];
}
}
/**
* Send configuration to LoRa node
*/
public function sendConfigToNode(string $nodeId, string $parameter, $value): array
{
try {
$configId = uniqid('CFG_');
$payload = LoRaMessage::generateConfigPayload($parameter, $value, $configId);
$message = LoRaMessage::create([
'node_id' => $nodeId,
'gateway_id' => 'GATEWAY_001',
'direction' => 'outbound',
'message_type' => 'config',
'payload' => $payload,
'status' => 'pending',
'metadata' => [
'config_id' => $configId,
'parameter' => $parameter,
'value' => $value
],
'transmitted_at' => now()
]);
$result = $this->transmitMessage($message);
return array_merge($result, [
'message_id' => $message->id,
'config_id' => $configId
]);
} catch (\Exception $e) {
return ['success' => false, 'error' => $e->getMessage()];
}
}
}