212 lines
8.6 KiB
PHP
212 lines
8.6 KiB
PHP
<?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,
|
||
]);
|
||
}
|
||
}
|
||
}
|