TKK_E32230220/app/Console/Commands/MqttSubscriberCommand.php

212 lines
8.6 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Console\Commands;
use App\Models\SortingHistory;
use App\Models\SortingSession;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use PhpMqtt\Client\MqttClient;
use PhpMqtt\Client\ConnectionSettings;
class MqttSubscriberCommand extends Command
{
/**
* The name and signature of the console command.
*/
protected $signature = 'mqtt:subscribe';
/**
* The console command description.
*/
protected $description = 'Subscribe ke MQTT broker (HiveMQ) dan simpan data sensor ke MongoDB';
/**
* Execute the console command.
*/
public function handle(): int
{
$host = config('services.mqtt.host');
$port = (int) config('services.mqtt.port', 8883);
$username = config('services.mqtt.username');
$password = config('services.mqtt.password');
$topic = config('services.mqtt.topic', 'fruit/sorting/data');
$clientId = config('services.mqtt.client_id', 'laravel-subscriber-' . uniqid());
$verifyTls = (bool) env('MQTT_TLS_VERIFY', false);
if (empty($host) || empty($username) || empty($password)) {
$this->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,
]);
}
}
}