error('❌ MQTT belum dikonfigurasi. Isi MQTT_HOST, MQTT_USERNAME, MQTT_PASSWORD di .env'); return self::FAILURE; } $this->info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); $this->info("🔌 MQTT Subscriber — FruitDashboard"); $this->info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); $this->info(" Broker : {$host}:{$port}"); $this->info(" Topic : {$topic}"); $this->info(" Client : {$clientId}"); $this->info(" TLS Ver.: " . ($verifyTls ? 'Enabled' : 'Bypassed (Local Dev)')); $this->info("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); $this->info(""); // Setup settings $connectionSettings = (new ConnectionSettings()) ->setUsername($username) ->setPassword($password) ->setUseTls(true) ->setTlsVerifyPeer($verifyTls) ->setTlsVerifyPeerName($verifyTls) ->setTlsSelfSignedAllowed(true) ->setKeepAliveInterval(60) ->setConnectTimeout(10); $this->info("📡 Memulai listener MQTT. Tekan Ctrl+C untuk berhenti."); $this->info(""); while (true) { try { $mqtt = new MqttClient($host, $port, $clientId); $this->info("⏳ Menghubungkan ke HiveMQ..."); $mqtt->connect($connectionSettings, true); $this->info("✅ Terhubung ke HiveMQ!"); Log::info("MQTT Subscriber terhubung ke {$host}:{$port}"); // Subscribe ke topic $mqtt->subscribe($topic, function (string $topic, string $message) { $this->processMessage($topic, $message); }, 1); $this->info("📡 Mendengarkan pesan di topic: {$topic}"); $this->info(""); // Loop terus mendengarkan pesan $mqtt->loop(true); // Disconnect jika keluar loop secara normal $mqtt->disconnect(); break; } catch (\Exception $e) { $this->error("⚠️ Koneksi terputus atau gagal: " . $e->getMessage()); Log::error("MQTT connection error, reconnecting in 5s: " . $e->getMessage()); $this->warn("⏳ Menghubungkan kembali dalam 5 detik..."); $this->info(""); sleep(5); } } return self::SUCCESS; } /** * Proses pesan yang diterima dari MQTT broker. * * Format JSON yang diharapkan dari ESP32/Arduino: * { * "raw_banana_weight": 1.50, * "ripe_banana_weight": 0.80, * "raw_orange_weight": 2.00, * "ripe_orange_weight": 1.20 * } */ protected function processMessage(string $topic, string $message): void { $timestamp = now()->format('H:i:s'); $this->line("[{$timestamp}] 📨 Pesan diterima dari topic: {$topic}"); try { $data = json_decode($message, true); if (json_last_error() !== JSON_ERROR_NONE) { $this->warn(" ⚠️ Format JSON tidak valid: {$message}"); Log::warning("MQTT: Pesan JSON tidak valid", ['raw' => $message]); return; } $this->line(" 📦 Data: " . json_encode($data)); // Validasi field yang diperlukan $required = ['raw_banana_weight', 'ripe_banana_weight', 'raw_orange_weight', 'ripe_orange_weight']; foreach ($required as $field) { if (!isset($data[$field]) || !is_numeric($data[$field])) { $this->warn(" ⚠️ Field '{$field}' tidak valid atau tidak ada."); Log::warning("MQTT: Field {$field} missing/invalid", $data); return; } } // Cek apakah ada sesi sortir yang aktif $session = SortingSession::active()->first(); if (!$session) { $this->warn(" ⏸️ Tidak ada sesi sortir aktif. Data diabaikan."); $this->warn(" Tekan 'Mulai Sortir' di dashboard terlebih dahulu."); Log::info("MQTT: Data diterima tapi tidak ada sesi aktif, diabaikan."); return; } // 1. Abaikan data jika semua berat adalah 0 (tidak ada buah di timbangan) $hasWeight = (float) $data['raw_banana_weight'] > 0 || (float) $data['ripe_banana_weight'] > 0 || (float) $data['raw_orange_weight'] > 0 || (float) $data['ripe_orange_weight'] > 0; if (!$hasWeight) { $this->line(" ℹ️ Data kosong (semua berat 0), dilewati."); return; } // 2. Cegah duplikasi data untuk buah yang sama dalam rentang waktu singkat (< 3 detik) $lastRecord = SortingHistory::where('session_id', (string) $session->_id) ->latest('classification_date') ->first(); if ($lastRecord) { $timeDiff = now()->diffInSeconds($lastRecord->classification_date); if ($timeDiff < 3) { $isDuplicate = abs((float)$lastRecord->raw_banana_weight - (float)$data['raw_banana_weight']) < 0.1 && abs((float)$lastRecord->ripe_banana_weight - (float)$data['ripe_banana_weight']) < 0.1 && abs((float)$lastRecord->raw_orange_weight - (float)$data['raw_orange_weight']) < 0.1 && abs((float)$lastRecord->ripe_orange_weight - (float)$data['ripe_orange_weight']) < 0.1; if ($isDuplicate) { $this->line(" ℹ️ Data duplikat untuk buah yang sama dalam waktu < 3 detik, dilewati."); return; } } } // Simpan ke MongoDB $record = SortingHistory::create([ 'session_id' => (string) $session->_id, 'classification_date' => now(), 'raw_banana_weight' => (float) $data['raw_banana_weight'], 'ripe_banana_weight' => (float) $data['ripe_banana_weight'], 'raw_orange_weight' => (float) $data['raw_orange_weight'], 'ripe_orange_weight' => (float) $data['ripe_orange_weight'], ]); $this->info(" ✅ Tersimpan ke MongoDB (ID: {$record->_id})"); $this->info(" 🍌 Pisang: mentah={$data['raw_banana_weight']}gram, matang={$data['ripe_banana_weight']}gram"); $this->info(" 🍊 Jeruk: mentah={$data['raw_orange_weight']}gram, matang={$data['ripe_orange_weight']}gram"); $this->info(" 📂 Sesi: {$session->_id}"); $this->info(""); Log::info("MQTT: Data tersimpan", [ 'record_id' => (string) $record->_id, 'session_id' => (string) $session->_id, 'data' => $data, ]); } catch (\Exception $e) { $this->error(" ❌ Gagal menyimpan data: " . $e->getMessage()); Log::error("MQTT: Gagal simpan data", [ 'error' => $e->getMessage(), 'message' => $message, ]); } } }