From 4448c83266ce503df4e3f6fa2000ff700aba5b1b Mon Sep 17 00:00:00 2001 From: rendygaafk Date: Sun, 11 May 2025 06:51:06 +0700 Subject: [PATCH] fix all pengumuman --- app/Events/BellRingEvent.php | 20 -- app/Http/Controllers/API/BellController.php | 74 ++++- .../Controllers/AnnouncementController.php | 302 ++++++++++++------ app/Http/Controllers/belController.php | 108 ++++--- app/Services/MqttService.php | 188 ++++++++++- .../views/admin/announcement/index.blade.php | 16 +- .../admin/bel/partials/scripts.blade.php | 2 +- routes/api.php | 22 +- routes/web.php | 9 - 9 files changed, 543 insertions(+), 198 deletions(-) delete mode 100644 app/Events/BellRingEvent.php diff --git a/app/Events/BellRingEvent.php b/app/Events/BellRingEvent.php deleted file mode 100644 index 1dc3270..0000000 --- a/app/Events/BellRingEvent.php +++ /dev/null @@ -1,20 +0,0 @@ -data = $data; - } - - public function broadcastOn() { - return new Channel('bell-channel'); - } -} \ No newline at end of file diff --git a/app/Http/Controllers/API/BellController.php b/app/Http/Controllers/API/BellController.php index c8ef7a5..6f1eeb0 100644 --- a/app/Http/Controllers/API/BellController.php +++ b/app/Http/Controllers/API/BellController.php @@ -5,46 +5,100 @@ use App\Http\Controllers\Controller; use App\Models\BellHistory; use Illuminate\Http\Request; +use Illuminate\Support\Carbon; class BellController extends Controller { /** - * Menyimpan data event bel dari ESP32 + * Menyimpan data event bel manual dari ESP32 + */ + public function storeManualEvent(Request $request) + { + return $this->storeEvent($request, 'manual'); + } + + /** + * Menyimpan data event bel schedule dari ESP32 */ public function storeScheduleEvent(Request $request) + { + return $this->storeEvent($request, 'schedule'); + } + + /** + * Fungsi privat untuk menyimpan event (digunakan oleh manual dan schedule) + */ + private function storeEvent(Request $request, string $triggerType) { $validated = $request->validate([ 'hari' => 'required|in:Senin,Selasa,Rabu,Kamis,Jumat,Sabtu,Minggu', 'waktu' => 'required|date_format:H:i:s', 'file_number' => 'required|string|size:4', 'volume' => 'sometimes|integer|min:0|max:30', - 'repeat' => 'sometimes|integer|min:1|max:5' + 'repeat' => 'sometimes|integer|min:1|max:5', + 'trigger_type' => 'sometimes|in:manual,schedule' // jika dikirim dari client ]); - // Tambahkan trigger_type secara otomatis - $validated['trigger_type'] = 'schedule'; - $validated['ring_time'] = now(); + // Set default values jika tidak ada + $validated['volume'] = $validated['volume'] ?? 15; + $validated['repeat'] = $validated['repeat'] ?? 1; + + // Override trigger_type dengan nilai dari parameter + $validated['trigger_type'] = $triggerType; + $validated['ring_time'] = Carbon::now(); + + $exists = BellHistory::where('hari', $validated['hari']) + ->where('waktu', $validated['waktu']) + ->where('file_number', $validated['file_number']) + ->where('trigger_type', $triggerType) + ->where('ring_time', '>=', Carbon::now()->subSeconds(60)) + ->exists(); + + if ($exists) { + return response()->json([ + 'success' => false, + 'message' => 'Event already recorded recently, skipping duplicate.' + ], 409); + } $history = BellHistory::create($validated); return response()->json([ 'success' => true, + 'message' => 'Bell event recorded successfully', 'data' => $history ], 201); } /** - * Mengambil data history untuk ESP32 (jika diperlukan) + * Mengambil data history untuk ESP32 */ public function getHistory(Request $request) { - $histories = BellHistory::orderBy('ring_time', 'desc') - ->limit(50) - ->get(); + $request->validate([ + 'limit' => 'sometimes|integer|min:1|max:100', + 'days' => 'sometimes|integer|min:1|max:30', + 'trigger_type' => 'sometimes|in:manual,schedule' + ]); + + $query = BellHistory::query() + ->orderBy('ring_time', 'desc'); + + if ($request->has('trigger_type')) { + $query->where('trigger_type', $request->trigger_type); + } + + if ($request->has('days')) { + $query->where('ring_time', '>=', Carbon::now()->subDays($request->days)); + } + + $limit = $request->input('limit', 50); + $histories = $query->limit($limit)->get(); return response()->json([ 'success' => true, + 'count' => $histories->count(), 'data' => $histories ]); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/AnnouncementController.php b/app/Http/Controllers/AnnouncementController.php index 3e8c549..7896771 100644 --- a/app/Http/Controllers/AnnouncementController.php +++ b/app/Http/Controllers/AnnouncementController.php @@ -6,148 +6,234 @@ use App\Models\Ruangan; use App\Http\Requests\StoreAnnouncementRequest; use Illuminate\Http\Request; -use PhpMqtt\Client\Facades\MQTT; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; +use App\Services\MqttService; class AnnouncementController extends Controller { + // Constants for modes and actions + const MODE_TTS = 'tts'; + const MODE_MANUAL = 'manual'; + const ACTION_ACTIVATE = 'activate'; + const ACTION_DEACTIVATE = 'deactivate'; + public function index() { - $ruangans = Ruangan::orderBy('nama_ruangan')->get(); - $mqttStatus = $this->checkMqttConnection(); - return view('admin.announcement.index', compact('ruangans', 'mqttStatus')); + try { + $ruangans = Ruangan::orderBy('nama_ruangan')->get(); + $mqttService = app(MqttService::class); + + return view('admin.announcement.index', [ + 'ruangans' => $ruangans, + 'mqttStatus' => $mqttService->isConnected(), + 'modes' => [self::MODE_TTS, self::MODE_MANUAL] + ]); + + } catch (\Exception $e) { + Log::error('Failed to load announcement index: ' . $e->getMessage()); + return back()->with('error', 'Gagal memuat halaman pengumuman'); + } } public function history() - { - $announcements = Announcement::with('ruangans') - ->when(request('mode'), function($query, $mode) { - return $query->where('mode', $mode); - }) - ->when(request('date'), function($query, $date) { - return $query->whereDate('sent_at', $date); - }) - ->orderBy('sent_at', 'desc') - ->paginate(10); - - return view('admin.announcement.history', compact('announcements')); - } - - private function checkMqttConnection() { try { - $mqtt = MQTT::connection(); - return $mqtt->isConnected(); + $announcements = Announcement::with('ruangans') + ->when(request('mode'), function($query, $mode) { + return $query->where('mode', $mode); + }) + ->when(request('date'), function($query, $date) { + return $query->whereDate('sent_at', $date); + }) + ->when(request('ruangan'), function($query, $ruanganId) { + return $query->whereHas('ruangans', function($q) use ($ruanganId) { + $q->where('ruangan.id', $ruanganId); + }); + }) + ->orderBy('sent_at', 'desc') + ->paginate(10) + ->withQueryString(); + + $ruangans = Ruangan::orderBy('nama_ruangan')->get(); + + return view('admin.announcement.history', [ + 'announcements' => $announcements, + 'ruangans' => $ruangans, + 'modes' => [self::MODE_TTS, self::MODE_MANUAL] + ]); + } catch (\Exception $e) { - return false; + Log::error('Failed to load announcement history: ' . $e->getMessage()); + return back()->with('error', 'Gagal memuat riwayat pengumuman'); } } public function store(StoreAnnouncementRequest $request) { - $announcementData = [ - 'mode' => $request->mode, - 'sent_at' => now(), - ]; - - // Hanya tambahkan message jika mode TTS - if ($request->mode === 'tts') { - $announcementData['message'] = $request->message; - } - - $announcement = Announcement::create($announcementData); - $announcement->ruangans()->sync($request->ruangans); - + DB::beginTransaction(); + try { - if ($request->mode === 'tts') { - MQTT::publish('control/relay', json_encode([ - 'mode' => 'tts', - 'ruang' => $request->ruangans - ])); - - MQTT::publish('tts/play', json_encode([ - 'ruang' => $request->ruangans, - 'teks' => $request->message - ])); - } else { - MQTT::publish('control/relay', json_encode([ - 'mode' => 'reguler', - 'ruang' => $request->ruangans - ])); + $announcementData = [ + 'mode' => $request->mode, + 'sent_at' => now(), + ]; + + if ($request->mode === self::MODE_TTS) { + $announcementData['message'] = $request->message; } - } catch (\Exception $e) { + + $announcement = Announcement::create($announcementData); + $announcement->ruangans()->sync($request->ruangans); + + $mqttService = app(MqttService::class); + + if ($request->mode === self::MODE_TTS) { + $success = $mqttService->sendTTSAnnouncement( + $request->ruangans, + $request->message + ); + } else { + $success = $mqttService->sendRelayControl( + 'activate', // Default action for announcements + $request->ruangans, + $request->mode + ); + } + + if (!$success) { + throw new \Exception('Gagal mengirim perintah ke perangkat'); + } + + DB::commit(); + return response()->json([ - 'message' => 'Gagal mengirim ke perangkat: ' . $e->getMessage() + 'success' => true, + 'message' => 'Pengumuman berhasil dikirim' + ]); + + } catch (\Exception $e) { + DB::rollBack(); + Log::error('Failed to store announcement: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengirim pengumuman: ' . $e->getMessage() ], 500); } - - return response()->json(['success' => true]); } - public function details($id) + + public function details($id) { - $announcement = Announcement::with('ruangans')->findOrFail($id); - return response()->json([ - 'mode' => $announcement->mode, - 'formatted_sent_at' => $announcement->formatted_sent_at, - 'message' => $announcement->message, - 'ruangans' => $announcement->ruangans->map(function($ruangan) { - return ['nama_ruangan' => $ruangan->nama_ruangan]; - }) - ]); + try { + $announcement = Announcement::with('ruangans')->findOrFail($id); + + return response()->json([ + 'success' => true, + 'data' => [ + 'mode' => $announcement->mode, + 'sent_at' => $announcement->sent_at->format('Y-m-d H:i:s'), + 'message' => $announcement->message, + 'ruangans' => $announcement->ruangans->map(function($ruangan) { + return [ + 'id' => $ruangan->id, + 'nama_ruangan' => $ruangan->nama_ruangan + ]; + }) + ] + ]); + + } catch (\Exception $e) { + Log::error('Failed to get announcement details: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil detail pengumuman' + ], 404); + } } public function destroy($id) { - $announcement = Announcement::findOrFail($id); - $announcement->delete(); - return response()->json(['success' => true]); + try { + $announcement = Announcement::findOrFail($id); + $announcement->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Pengumuman berhasil dihapus' + ]); + + } catch (\Exception $e) { + Log::error('Failed to delete announcement: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Gagal menghapus pengumuman' + ], 500); + } } public function checkMqtt() { + $mqttService = app(MqttService::class); return response()->json([ - 'connected' => $this->checkMqttConnection() + 'connected' => $mqttService->isConnected() ]); } public function controlRelay(Request $request) { - $request->validate([ + $validated = $request->validate([ 'ruangans' => 'required|array|min:1', 'ruangans.*' => 'exists:ruangan,id', - 'action' => 'required|in:activate,deactivate', - 'mode' => 'required|in:manual,tts' + 'action' => 'required|in:'.self::ACTION_ACTIVATE.','.self::ACTION_DEACTIVATE, + 'mode' => 'required|in:'.self::MODE_MANUAL.','.self::MODE_TTS ]); - $ruangans = Ruangan::whereIn('id', $request->ruangans)->get(); - $state = $request->action === 'activate' ? 'on' : 'off'; - + DB::beginTransaction(); + try { - // Kirim perintah ke ESP32 via MQTT - MQTT::publish('control/relay', json_encode([ - 'action' => $request->action, - 'ruang' => $request->ruangans, - 'mode' => $request->mode - ])); + $state = $validated['action'] === self::ACTION_ACTIVATE ? 'on' : 'off'; + $ruanganIds = $validated['ruangans']; - // Update status relay di database - Ruangan::whereIn('id', $request->ruangans)->update(['relay_state' => $state]); + $mqttService = app(MqttService::class); + $success = $mqttService->sendRelayControl( + $validated['action'], + $ruanganIds, + $validated['mode'] + ); - // Jika mengaktifkan relay, simpan sebagai pengumuman manual - if ($request->action === 'activate' && $request->mode === 'manual') { + if (!$success) { + throw new \Exception('Gagal mengirim perintah ke perangkat'); + } + + // Update database + Ruangan::whereIn('id', $ruanganIds)->update(['relay_state' => $state]); + + // Log manual activations as announcements + if ($validated['action'] === self::ACTION_ACTIVATE && $validated['mode'] === self::MODE_MANUAL) { $announcement = Announcement::create([ - 'mode' => 'manual', + 'mode' => self::MODE_MANUAL, 'message' => 'Pengumuman via microphone manual', 'sent_at' => now() ]); - $announcement->ruangans()->sync($request->ruangans); + $announcement->ruangans()->sync($ruanganIds); } - return response()->json(['success' => true]); + DB::commit(); + + return response()->json([ + 'success' => true, + 'message' => 'Relay berhasil dikontrol' + ]); } catch (\Exception $e) { + DB::rollBack(); + Log::error('Failed to control relay: ' . $e->getMessage()); + return response()->json([ + 'success' => false, 'message' => 'Gagal mengontrol relay: ' . $e->getMessage() ], 500); } @@ -155,7 +241,45 @@ public function controlRelay(Request $request) public function relayStatus() { - $ruangans = Ruangan::select('id', 'relay_state')->get(); - return response()->json($ruangans); + try { + $ruangans = Ruangan::select('id', 'nama_ruangan', 'relay_state')->get(); + + return response()->json([ + 'success' => true, + 'data' => $ruangans + ]); + + } catch (\Exception $e) { + Log::error('Failed to get relay status: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil status relay' + ], 500); + } + } + + public function announcementStatus(Request $request) + { + try { + $request->validate([ + 'ruangans' => 'required|array|min:1', + 'ruangans.*' => 'exists:ruangan,id' + ]); + + $mqttService = app(MqttService::class); + $statuses = $mqttService->getAnnouncementStatus($request->ruangans); + + return response()->json([ + 'success' => true, + 'data' => $statuses + ]); + + } catch (\Exception $e) { + Log::error('Failed to get announcement status: ' . $e->getMessage()); + return response()->json([ + 'success' => false, + 'message' => 'Gagal mengambil status pengumuman' + ], 500); + } } } \ No newline at end of file diff --git a/app/Http/Controllers/belController.php b/app/Http/Controllers/belController.php index 86c9467..9e83cc8 100644 --- a/app/Http/Controllers/belController.php +++ b/app/Http/Controllers/belController.php @@ -64,6 +64,19 @@ protected function handleBellEvent(string $message, string $triggerType): void try { $data = json_decode($message, true, 512, JSON_THROW_ON_ERROR); + // Cek duplikasi dalam 60 detik terakhir + $existing = BellHistory::where('file_number', $data['file_number']) + ->where('created_at', '>=', now()->subMinute()) + ->exists(); + + if ($existing) { + Log::warning("Duplicate bell event blocked", [ + 'type' => $triggerType, + 'data' => $data + ]); + return; + } + $requiredFields = ['hari', 'waktu', 'file_number']; foreach ($requiredFields as $field) { if (!isset($data[$field])) { @@ -97,16 +110,17 @@ protected function handleBellEvent(string $message, string $triggerType): void private function normalizeWaktu(?string $time): string { - if (empty($time)) { - return '00:00:00'; + if (empty($time)) return '00:00:00'; + + try { + return Carbon::createFromFormat('H:i:s', $time)->format('H:i:s'); + } catch (\Exception $e) { + try { + return Carbon::createFromFormat('H:i', $time)->format('H:i:s'); + } catch (\Exception $e) { + return '00:00:00'; + } } - - $parts = explode(':', $time); - $hour = min(23, max(0, (int)($parts[0] ?? 0))); - $minute = min(59, max(0, (int)($parts[1] ?? 0))); - $second = min(59, max(0, (int)($parts[2] ?? 0))); - - return sprintf('%02d:%02d:%02d', $hour, $minute, $second); } protected function handleStatusResponse(string $message): void @@ -449,44 +463,43 @@ public function status() } } + protected function getFormattedSchedules() + { + return JadwalBel::active() + ->get() + ->map(function ($item) { + return [ + 'hari' => $item->hari, + 'waktu' => Carbon::parse($item->waktu)->format('H:i'), // Ensure "H:i" format + 'file_number' => $item->file_number, + 'volume' => (int)$item->volume ?? 15, // Force integer type + 'repeat' => (int)$item->repeat ?? 1, // Force integer type + 'is_active' => (bool)$item->is_active // Force boolean + ]; + })->toArray(); + } + public function syncSchedule() { - try { - $schedules = JadwalBel::active() - ->get() - ->map(fn($item) => [ - 'hari' => $item->hari, - 'waktu' => Carbon::parse($item->waktu)->format('H:i'), - 'file_number' => $item->file_number - ]); - - $this->mqttService->publish( - $this->mqttConfig['topics']['commands']['sync'], - json_encode([ - 'action' => 'sync', - 'timestamp' => Carbon::now()->toDateTimeString(), - 'schedules' => $schedules - ]), - 1 - ); - - Status::updateOrCreate(['id' => 1], ['last_sync' => Carbon::now()]); - - return response()->json([ - 'success' => true, - 'message' => 'Jadwal berhasil disinkronisasi', - 'data' => [ - 'count' => $schedules->count(), - 'last_sync' => Carbon::now()->toDateTimeString() - ] - ]); - } catch (\Exception $e) { - Log::error('Gagal sync jadwal: ' . $e->getMessage()); - return response()->json([ - 'success' => false, - 'message' => 'Gagal menyinkronisasi jadwal: ' . $e->getMessage() - ], 500); - } + Log::debug('Sync request received from frontend'); + $payload = [ + 'action' => 'sync', + 'timestamp' => now()->toDateTimeString(), + 'schedules' => $this->getFormattedSchedules() + ]; + + $this->mqttService->publish( + 'bel/sekolah/command/sync', + json_encode($payload), + 1, // QoS 1 + false // Not retained + ); + + return response()->json([ + 'success' => true, + 'message' => 'Sync command sent', + 'payload' => $payload // For debugging + ]); } protected function syncSchedules(): void @@ -497,7 +510,10 @@ protected function syncSchedules(): void ->map(fn($item) => [ 'hari' => $item->hari, 'waktu' => Carbon::parse($item->waktu)->format('H:i:s'), - 'file_number' => $item->file_number + 'file_number' => $item->file_number, + 'volume' => $item->volume ?? 15, // Add this + 'repeat' => $item->repeat ?? 1, // Add this + 'is_active' => $item->is_active // Add this ]); $this->mqttService->publish( diff --git a/app/Services/MqttService.php b/app/Services/MqttService.php index 8c0e5ae..92aae8e 100644 --- a/app/Services/MqttService.php +++ b/app/Services/MqttService.php @@ -7,7 +7,6 @@ use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Cache; use App\Models\BellHistory; -use App\Events\BellRingEvent; use Carbon\Carbon; use Illuminate\Support\Facades\Validator; @@ -22,6 +21,12 @@ class MqttService protected const MAX_RECONNECT_ATTEMPTS = 5; protected const RECONNECT_DELAY = 5; + // Constants for announcement topics + protected const TOPIC_ANNOUNCEMENT_CONTROL = 'control/relay'; + protected const TOPIC_ANNOUNCEMENT_TTS = 'tts/play'; + protected const TOPIC_ANNOUNCEMENT_STATUS = 'announcement/status'; + protected const TOPIC_ANNOUNCEMENT_RESPONSE = 'announcement/response'; + public function __construct() { $this->config = config('mqtt'); @@ -40,12 +45,23 @@ protected function initializeConnection(): void $this->connect(); $this->subscribeToBellTopics(); + $this->subscribeToAnnouncementTopics(); // Add announcement subscriptions } catch (\Exception $e) { Log::error('MQTT Initialization failed: ' . $e->getMessage()); $this->scheduleReconnect(); } } + // In MqttService + public function getConnectionStatus(): array + { + return [ + 'connected' => $this->isConnected, + 'last_attempt' => Cache::get('mqtt_last_attempt'), + 'queued_messages' => count(Cache::get('mqtt_message_queue', [])) + ]; + } + protected function getConnectionConfig(): array { return $this->config['connections'][$this->config['default_connection']]; @@ -68,6 +84,146 @@ protected function subscribeToBellTopics(): void } } + /** + * Subscribe to announcement-related topics + */ + protected function subscribeToAnnouncementTopics(): void + { + $topics = [ + self::TOPIC_ANNOUNCEMENT_STATUS => fn($t, $m) => $this->handleAnnouncementStatus($m), + self::TOPIC_ANNOUNCEMENT_RESPONSE => fn($t, $m) => $this->handleAnnouncementResponse($m) + ]; + + foreach ($topics as $topic => $callback) { + $this->subscribe($topic, $callback); + } + } + + /** + * Handle announcement status updates from devices + */ + protected function handleAnnouncementStatus(string $message): void + { + try { + $data = json_decode($message, true, 512, JSON_THROW_ON_ERROR); + + Log::info('Announcement status update', [ + 'status' => $data['status'] ?? 'unknown', + 'ruang' => $data['ruang'] ?? null, + 'mode' => $data['mode'] ?? null, + 'timestamp' => $data['timestamp'] ?? null + ]); + + // Store status in cache for quick access + if (isset($data['ruang'])) { + Cache::put('announcement_status_'.$data['ruang'], $data['status'], 60); + } + + } catch (\Exception $e) { + Log::error('Failed to process announcement status', [ + 'error' => $e->getMessage(), + 'message' => $message + ]); + } + } + + /** + * Handle announcement responses from devices + */ + protected function handleAnnouncementResponse(string $message): void + { + try { + $data = json_decode($message, true, 512, JSON_THROW_ON_ERROR); + + Log::debug('Announcement response received', [ + 'type' => $data['type'] ?? 'unknown', + 'status' => $data['status'] ?? null, + 'ruang' => $data['ruang'] ?? null, + 'message' => $data['message'] ?? null + ]); + + // TODO: Add any specific response handling logic here + + } catch (\Exception $e) { + Log::error('Failed to process announcement response', [ + 'error' => $e->getMessage(), + 'message' => $message + ]); + } + } + + public function sendRelayControl(string $action, array $ruangans, string $mode = 'manual'): bool + { + $payload = [ + 'action' => $action, // 'activate' atau 'deactivate' + 'ruang' => $ruangans, + 'mode' => $mode, + 'timestamp' => now()->toDateTimeString() + ]; + + try { + return $this->publish(self::TOPIC_ANNOUNCEMENT_CONTROL, json_encode($payload)); + } catch (\Exception $e) { + Log::error('Failed to send relay control', [ + 'error' => $e->getMessage(), + 'payload' => $payload + ]); + return false; + } + } + + /** + * Mengirim pengumuman TTS + */ + public function sendTTSAnnouncement(array $ruangans, string $message, string $language = 'id-id'): bool + { + $payload = [ + 'ruang' => $ruangans, + 'teks' => $message, + 'hl' => $language, + 'timestamp' => now()->toDateTimeString() + ]; + + try { + return $this->publish(self::TOPIC_ANNOUNCEMENT_TTS, json_encode($payload)); + } catch (\Exception $e) { + Log::error('Failed to send TTS announcement', [ + 'error' => $e->getMessage(), + 'payload' => $payload + ]); + return false; + } + } + + /** + * Fungsi kompatibilitas backward (bisa dihapus setelah update controller) + * @deprecated + */ + public function sendAnnouncement(string $mode, array $ruangans, ?string $message = null): bool + { + if ($mode === 'tts' && $message) { + return $this->sendTTSAnnouncement($ruangans, $message); + } + + // Default action untuk mode manual + return $this->sendRelayControl('activate', $ruangans, $mode); + } + + + /** + * Get current announcement status for rooms + */ + public function getAnnouncementStatus(array $ruanganIds): array + { + $statuses = []; + + foreach ($ruanganIds as $id) { + $statuses[$id] = Cache::get('announcement_status_'.$id, 'unknown'); + } + + return $statuses; + } + protected function handleBellNotification(string $message, string $triggerType): void { Log::debug("Processing {$triggerType} bell event", compact('message', 'triggerType')); @@ -77,7 +233,11 @@ protected function handleBellNotification(string $message, string $triggerType): $history = $this->createBellHistory($data, $triggerType); $this->logBellEvent($history, $triggerType); - $this->dispatchBellEvent($history); + + Log::debug('Raw MQTT payload', [ + 'message' => $message, + 'trigger_type' => $triggerType + ]); } catch (\JsonException $e) { Log::error("Invalid JSON format in bell notification", [ 'error' => $e->getMessage(), @@ -144,17 +304,17 @@ protected function logBellEvent(BellHistory $history, string $triggerType): void ]); } - protected function dispatchBellEvent(BellHistory $history): void - { - event(new BellRingEvent([ - 'id' => $history->id, - 'hari' => $history->hari, - 'waktu' => $history->waktu, - 'file_number' => $history->file_number, - 'trigger_type' => $history->trigger_type, - 'ring_time' => $history->ring_time->toDateTimeString() - ])); - } + // protected function dispatchBellEvent(BellHistory $history): void + // { + // event(new BellRingEvent([ + // 'id' => $history->id, + // 'hari' => $history->hari, + // 'waktu' => $history->waktu, + // 'file_number' => $history->file_number, + // 'trigger_type' => $history->trigger_type, + // 'ring_time' => $history->ring_time->toDateTimeString() + // ])); + // } private function normalizeTime(string $time): string { @@ -251,7 +411,7 @@ public function subscribe(string $topic, callable $callback, int $qos = 0): bool } } - public function publish(string $topic, string $message, int $qos = 0, bool $retain = false): bool + public function publish(string $topic, string $message, int $qos = 1, bool $retain = false): bool { if (!$this->isConnected && !$this->connect()) { $this->queueMessage($topic, $message, $qos, $retain); diff --git a/resources/views/admin/announcement/index.blade.php b/resources/views/admin/announcement/index.blade.php index b48df79..c8cf88b 100644 --- a/resources/views/admin/announcement/index.blade.php +++ b/resources/views/admin/announcement/index.blade.php @@ -56,7 +56,7 @@ class="flex items-center px-5 py-2.5 bg-white border border-blue-500 text-blue-6
-
+ @csrf @@ -100,10 +100,10 @@ class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"> {{ $ruangan->relay_state === 'on' ? 'AKTIF' : 'NONAKTIF' }}
-

+

@endforeach @@ -126,7 +126,7 @@ class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"> @endforeach @@ -482,7 +482,7 @@ function switchTab(activeTab, inactiveTab, activeBtn, inactiveBtn) { // Check relay status periodically function checkRelayStatus() { - $.get("{{ route('admin.announcement.relay-status') }}", function(data) { + $.get("{{ url('/api/announcements/relay/status') }}", function(data) { data.forEach(room => { const statusElement = $(`#status-ruang-${room.id}`); if (statusElement.length) { @@ -496,7 +496,7 @@ function checkRelayStatus() { // Check MQTT connection status function checkMqttStatus() { - $.get("{{ route('admin.check.mqtt') }}", function(data) { + $.get("{{ url('/api/announcements/mqtt/status') }}", function(data) { const statusElement = $('.mqtt-status-indicator'); const textElement = $('.mqtt-status-text'); diff --git a/resources/views/admin/bel/partials/scripts.blade.php b/resources/views/admin/bel/partials/scripts.blade.php index d2eacd3..16046ce 100644 --- a/resources/views/admin/bel/partials/scripts.blade.php +++ b/resources/views/admin/bel/partials/scripts.blade.php @@ -274,7 +274,7 @@ class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue- } // Ring bell function -function ringBell(fileNumber, volume = 20) { +function ringBell(fileNumber, volume = 15) { showLoading('Ringing bell...'); fetch("{{ route('api.bel.ring') }}", { method: 'POST', diff --git a/routes/api.php b/routes/api.php index fe031b7..29686df 100644 --- a/routes/api.php +++ b/routes/api.php @@ -5,6 +5,7 @@ use App\Http\Controllers\PresensiController; use App\Http\Controllers\SiswaController; use App\Http\Controllers\API\BellController; +use App\Http\Controllers\AnnouncementController; Route::prefix('bel')->group(function () { Route::post('/ring', [BelController::class, 'ring'])->name('api.bel.ring'); @@ -18,9 +19,28 @@ }); // Endpoint untuk menerima data bel dari ESP32 -Route::post('/bell-events', [BellController::class, 'storeScheduleEvent']); +Route::post('/bell-events/manual', [BellController::class, 'storeManualEvent']); +Route::post('/bell-events/schedule', [BellController::class, 'storeScheduleEvent']); Route::get('/bell-history', [BellController::class, 'getHistory']); + +Route::prefix('announcements')->group(function () { + // CRUD operations + Route::post('/store', [AnnouncementController::class, 'store']); + Route::get('/{id}', [AnnouncementController::class, 'details']); + Route::delete('/{id}', [AnnouncementController::class, 'destroy']); + + // Status checks + Route::get('/mqtt/status', [AnnouncementController::class, 'checkMqtt']); + + // Relay control + Route::post('/relay/control', [AnnouncementController::class, 'controlRelay']); + Route::get('/relay/status', [AnnouncementController::class, 'relayStatus']); + + // Announcement status + Route::post('/status', [AnnouncementController::class, 'announcementStatus']); +}); + #Presensi Route::post('/presensi', [PresensiController::class, 'store']); diff --git a/routes/web.php b/routes/web.php index 188ccf6..39f678e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -106,19 +106,10 @@ }); }); - // Announcement System // Announcement System Route::prefix('announcement')->controller(AnnouncementController::class)->group(function () { Route::get('/', 'index')->name('admin.announcement.index'); Route::get('/history', 'history')->name('admin.announcement.history'); - Route::post('/', 'store')->name('admin.announcement.store'); - Route::get('/{id}/details', 'details')->name('admin.announcement.details'); - Route::delete('/{id}', 'destroy')->name('admin.announcement.destroy'); - - // MQTT & Relay - Route::get('/check/mqtt', 'checkMqtt')->name('admin.check.mqtt'); - Route::post('/control-relay', 'controlRelay')->name('admin.announcement.control-relay'); - Route::get('/relay-status', 'relayStatus')->name('admin.announcement.relay-status'); }); });