connect(null, false); $payload = is_array($message) ? json_encode($message) : (string)$message; $mqtt->publish($topic, $payload, 0); $mqtt->disconnect(); } catch (\Exception $e) { \Log::error("MQTT Error: " . $e->getMessage()); } } } /* ================= 1. VERIFIKASI RFID (IDENTIFIKASI SEMENTARA) ================= */ Route::get('/esp/check-member', function (Request $request) { $uid = cleanData($request->query('uid')); if (!$uid) return response()->json(['status' => 'error', 'message' => 'UID Kosong'], 400); $iot = IotMode::find(1); // Tambahan: Cegah proses jika sistem sedang dalam state lock 'failed' if ($iot && $iot->mode === 'wait') { return response()->json(['status' => 'error', 'message' => 'Sistem terkunci sementara'], 423); } $anggota = DB::table('anggota')->where('uid', $uid)->orWhere('uid_clean', $uid)->first(); if (!$anggota) { // Jika kartu salah, ubah mode jadi failed untuk mengunci scan ganda if ($iot) $iot->update(['mode' => 'wait']); mqttPublish('rfid/status', 'failed'); mqttPublish('rfid/ui/member', ['status' => 'invalid']); return response()->json(['status' => 'gagal', 'message' => "Kartu $uid tidak terdaftar"], 404); } if ($iot && $iot->book_id && ($iot->mode === 'wait' || $iot->mode === 'identified')) { $iot->update([ 'mode' => 'identified', 'user_id' => $anggota->id ]); mqttPublish('rfid/status', 'valid'); mqttPublish('rfid/ui/member', [ 'status' => 'valid', 'nama' => $anggota->nama, 'uid' => $uid ]); return response()->json([ 'status' => 'berhasil', 'id' => $anggota->id, 'nama' => $anggota->nama, 'message'=> 'Anggota teridentifikasi, silakan klik lanjutkan di layar.' ], 200); } return response()->json(['status' => 'error', 'message' => 'Sistem tidak siap'], 400); }); /* ================= 2. KONFIRMASI PEMINJAMAN REAL ================= */ Route::get('/esp/konfirmasi-pinjam', function () { $iot = IotMode::find(1); /* |-------------------------------------------------------------------------- | Validasi State |-------------------------------------------------------------------------- */ if ( !$iot || !$iot->book_id || !$iot->user_id ) { return response()->json([ 'status' => 'error', 'message' => 'Data tidak lengkap' ], 400); } /* |-------------------------------------------------------------------------- | Pastikan Mode Sudah Identified |-------------------------------------------------------------------------- */ if ($iot->mode !== 'identified') { return response()->json([ 'status' => 'error', 'message' => 'Sistem belum siap' ], 400); } try { /* |-------------------------------------------------------------------------- | Lock State |-------------------------------------------------------------------------- */ $iot->update([ 'mode' => 'processing' ]); /* |-------------------------------------------------------------------------- | MQTT Feedback ke ESP32 |-------------------------------------------------------------------------- */ mqttPublish('rfid/status', 'processing'); /* |-------------------------------------------------------------------------- | Dispatch Queue |-------------------------------------------------------------------------- */ ProcessPeminjamanJob::dispatch( $iot->book_id, $iot->user_id ); /* |-------------------------------------------------------------------------- | Response Cepat ke Frontend |-------------------------------------------------------------------------- */ return response()->json([ 'status' => 'queued', 'message' => 'Peminjaman masuk queue' ]); } catch (\Throwable $e) { /* |-------------------------------------------------------------------------- | Rollback State |-------------------------------------------------------------------------- */ $iot->update([ 'mode' => 'wait' ]); mqttPublish('rfid/status', 'failed'); \Log::error( 'Dispatch Queue Error: ' . $e->getMessage() ); return response()->json([ 'status' => 'error', 'message' => 'Queue gagal diproses' ], 500); } }); /* ================= 3. SCAN BUKU (PINJAM VS KEMBALI) ================= */ Route::post('/esp/scan-buku', function (Request $request) { $barcode = cleanData($request->barcode); if (!$barcode) return response()->json(['status' => 'barcode_kosong']); $book = DB::table('koleksis') ->where('kode_eksemplar', $barcode) ->orWhere('barcode', $barcode) ->first(); if (!$book) return response()->json(['status' => 'tidak_ditemukan']); $isBorrowed = DB::table('borrow_transactions') ->where('koleksi_id', $book->biblio_id) ->where('status', 'dipinjam') ->first(); if ($isBorrowed) { $peminjam = DB::table('anggota')->where('id', $isBorrowed->anggota_id)->first(); IotMode::updateOrCreate(['id' => 1], [ 'mode' => 'return_confirm', 'book_id' => $book->biblio_id, 'user_id' => $peminjam->id ]); return response()->json([ 'status' => 'konfirmasi_kembali', 'book' => [ 'judul' => $book->judul_koleksi ?? $book->title, 'barcode' => $book->kode_eksemplar ?? $book->barcode, 'peminjam_saat_ini' => $peminjam ? $peminjam->nama : 'Anggota', 'tanggal_pinjam' => $isBorrowed->tanggal_pinjam ] ]); } IotMode::updateOrCreate(['id' => 1], [ 'mode' => 'wait', 'book_id' => $book->biblio_id, 'user_id' => null ]); mqttPublish('rfid/mode', 'wait'); return response()->json([ 'status' => 'ok', 'book' => [ 'judul' => $book->judul_koleksi ?? $book->title, 'barcode' => $book->kode_eksemplar ?? $book->barcode ] ]); }); /* ================= 4. PROSES PENGEMBALIAN ================= */ Route::get('/esp/proses-kembali', function () { $iot = IotMode::find(1); if ( !$iot || $iot->mode !== 'return_confirm' || !$iot->book_id ) { return response()->json([ 'status' => 'error', 'message' => 'Sistem tidak dalam mode pengembalian' ], 400); } try { // LOCK MODE $iot->update([ 'mode' => 'processing_return' ]); // DISPATCH JOB KE QUEUE ProcessPengembalianJob::dispatch([ 'book_id' => $iot->book_id, 'user_id' => $iot->user_id ]); // MQTT FEEDBACK CEPAT KE ESP32 mqttPublish('rfid/status', 'processing'); return response()->json([ 'status' => 'queued', 'message' => 'Pengembalian diproses queue' ]); } catch (\Throwable $e) { logger()->error($e); mqttPublish('rfid/status', 'error'); return response()->json([ 'status' => 'error', 'message' => $e->getMessage() ], 500); } }); /* ================= 5. STATE, RESET, & BATAL ================= */ Route::get('/esp/state', function () { $iot = IotMode::find(1); if (!$iot) return response()->json(['status' => 'empty', 'mode' => 'standby']); $book = $iot->book_id ? DB::table('koleksis')->where('biblio_id', $iot->book_id)->first() : null; $user = $iot->user_id ? DB::table('anggota')->where('id', $iot->user_id)->first() : null; return response()->json([ 'status' => 'ok', 'mode' => $iot->mode, 'book' => $book ? [ 'judul' => $book->judul_koleksi ?? $book->title, 'barcode' => $book->kode_eksemplar ?? $book->barcode ] : null, 'user' => $user ? [ 'nama' => $user->nama ] : null ]); }); Route::get('/esp/batalkan-transaksi', function () { // Reset SQL ke mode standby IotMode::where('id', 1)->update(['mode' => 'standby', 'book_id' => null, 'user_id' => null]); // Sinkronkan ke Hardware mqttPublish('rfid/mode', 'standby'); mqttPublish('rfid/status', 'idle'); mqttPublish('rfid/cancel', 'reset'); // Signal tambahan jika Backbone MQTT memantau topik rfid/cancel return response()->json(['status' => 'ok', 'message' => 'Transaksi dibatalkan']); }); Route::get('/esp/selesai-transaksi', function() { IotMode::where('id', 1)->update(['mode' => 'standby', 'book_id' => null, 'user_id' => null]); mqttPublish('rfid/mode', 'standby'); mqttPublish('rfid/status', 'idle'); return response()->json(['status' => 'ok']); }); Route::get('/esp/reset', function () { IotMode::where('id', 1)->update(['mode' => 'standby', 'book_id' => null, 'user_id' => null]); mqttPublish('rfid/mode', 'standby'); mqttPublish('rfid/status', 'idle'); return response()->json(['status' => 'ok']); }); /* ================= 6. ADMIN REGISTER MODE ================= */ Route::get('/esp/start-register/{user_id}', function ($user_id) { try { IotMode::updateOrCreate(['id' => 1], [ 'mode' => 'admin', 'user_id' => $user_id, 'book_id' => null ]); mqttPublish('rfid/mode', 'admin'); return response()->json([ 'status' => 'ready', 'message' => 'Alat dalam mode pendaftaran. Silakan tap kartu.' ]); } catch (\Exception $e) { return response()->json(['status' => 'error'], 500); } }); /* ================= 7. CONTROLLER BINDINGS ================= */ Route::get('/esp/konfirmasi-kembali/{id}', [UserPeminjamanController::class, 'kembalikan']); Route::get('/esp/konfirmasi-kembali', [PeminjamanController::class, 'konfirmasiKembali']);