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()]; } } }