TKK_E3220375/app/Http/Controllers/AnnouncementController.php

356 lines
12 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Announcement;
use App\Models\Ruangan;
use App\Services\MqttService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Storage;
use Carbon\Carbon;
class AnnouncementController extends Controller
{
// Mode constants
const MODE_REGULER = 'reguler';
const MODE_TTS = 'tts';
// Relay constants
const RELAY_ON = 'ON';
const RELAY_OFF = 'OFF';
// TTS API constants
const TTS_API_URL = 'http://api.voicerss.org/';
const TTS_API_KEY = '90927de8275148d79080facd20fb486c';
const TTS_DEFAULT_VOICE = 'id-id';
const TTS_DEFAULT_SPEED = 0;
const TTS_DEFAULT_FORMAT = 'wav';
protected $mqttService;
protected $mqttConfig;
public function __construct(MqttService $mqttService)
{
$this->mqttService = $mqttService;
$this->mqttConfig = config('mqtt');
$this->initializeMqttSubscriptions();
}
protected function initializeMqttSubscriptions()
{
try {
$this->mqttService->subscribe(
$this->mqttConfig['topics']['responses']['announcement_ack'],
function (string $topic, string $message) {
$this->handleAnnouncementAck($message);
}
);
$this->mqttService->subscribe(
$this->mqttConfig['topics']['responses']['announcement_error'],
function (string $topic, string $message) {
$this->handleAnnouncementError($message);
}
);
$this->mqttService->subscribe(
$this->mqttConfig['topics']['responses']['relay_status'],
function (string $topic, string $message) {
$this->handleRelayStatusUpdate($message);
}
);
} catch (\Exception $e) {
Log::error('MQTT Subscription Error: ' . $e->getMessage());
}
}
public function index()
{
$ruangan = Ruangan::with(['kelas', 'jurusan'])->get();
$announcements = Announcement::with(['ruangans'])
->latest()
->paginate(10);
try {
$mqttStatus = $this->mqttService->isConnected() ? 'Connected' : 'Disconnected';
} catch (\Exception $e) {
$mqttStatus = 'Disconnected';
Log::error('MQTT check failed: ' . $e->getMessage());
}
return view('admin.announcement.index', [
'ruangans' => $ruangan,
'announcements' => $announcements,
'modes' => [self::MODE_REGULER, self::MODE_TTS],
'relayStates' => [self::RELAY_ON, self::RELAY_OFF],
'mqttStatus' => $mqttStatus
]);
}
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'mode' => 'required|in:reguler,tts',
'ruangans' => 'required|array',
'ruangans.*' => 'exists:ruangan,id',
'relay_action' => 'required_if:mode,reguler|in:ON,OFF',
'tts_text' => 'required_if:mode,tts|string|max:1000',
'tts_voice' => 'required_if:mode,tts',
'tts_speed' => 'required_if:mode,tts|integer|min:-10|max:10',
]);
if ($validator->fails()) {
return redirect()->back()
->withErrors($validator)
->withInput();
}
try {
$announcement = new Announcement();
$announcement->mode = $request->mode;
if ($request->mode === self::MODE_REGULER) {
$announcement->message = $request->relay_action === self::RELAY_ON
? 'Aktivasi Relay Ruangan'
: 'Deaktivasi Relay Ruangan';
$announcement->is_active = $request->relay_action === self::RELAY_ON;
$announcement->relay_state = $request->relay_action;
} else {
$audioContent = $this->generateTTS(
$request->tts_text,
$request->tts_voice,
$request->tts_speed
);
if (!$audioContent) {
throw new \Exception('Failed to generate TTS audio');
}
$fileName = 'tts/' . now()->format('YmdHis') . '.wav';
Storage::disk('public')->put($fileName, $audioContent);
$announcement->message = $request->tts_text;
$announcement->audio_path = $fileName;
$announcement->voice = $request->tts_voice;
$announcement->speed = $request->tts_speed;
$announcement->relay_state = self::RELAY_OFF; // Default untuk TTS
}
$announcement->sent_at = now();
$announcement->status = 'pending';
if (!$announcement->save()) {
throw new \Exception('Failed to save announcement');
}
$existingRuangan = Ruangan::whereIn('id', $request->ruangans)->pluck('id');
if ($existingRuangan->count() != count($request->ruangans)) {
throw new \Exception('Some selected ruangan not found');
}
$announcement->ruangans()->sync($existingRuangan);
$this->publishAnnouncement($announcement);
return redirect()->route('announcement.index')
->with('success', 'Pengumuman berhasil dikirim');
} catch (\Exception $e) {
Log::error('Announcement Error: ' . $e->getMessage());
if (isset($announcement) && $announcement->exists) {
$announcement->delete();
}
return redirect()->back()
->with('error', 'Gagal: ' . $e->getMessage())
->withInput();
}
}
protected function publishAnnouncement(Announcement $announcement)
{
$payload = [
'mode' => $announcement->mode,
'announcement_id' => $announcement->id,
'ruangans' => $announcement->ruangans->pluck('nama_ruangan')->toArray(),
'timestamp' => now()->toDateTimeString()
];
if ($announcement->mode === self::MODE_REGULER) {
$payload['relay_state'] = $announcement->relay_state;
// Kirim perintah relay ke masing-masing ruangan
foreach ($announcement->ruangans as $ruangan) {
$topic = $ruangan->mqtt_topic ?? "ruangan/{$ruangan->id}/relay/control";
$this->mqttService->publish(
$topic,
json_encode([
'state' => $announcement->relay_state,
'announcement_id' => $announcement->id
]),
1 // QoS level
);
// Update status relay di database
$ruangan->update(['relay_state' => $announcement->relay_state]);
}
} else {
$payload['message'] = $announcement->message;
$payload['audio_url'] = asset('storage/' . $announcement->audio_path);
$payload['voice'] = $announcement->voice;
$payload['speed'] = $announcement->speed;
}
// Publis ke topic announcement umum
$this->mqttService->publish(
$this->mqttConfig['topics']['commands']['announcement'],
json_encode($payload),
1
);
}
protected function generateTTS($text, $voice, $speed)
{
$response = Http::get(self::TTS_API_URL, [
'key' => self::TTS_API_KEY,
'hl' => $voice,
'src' => $text,
'r' => $speed,
'c' => self::TTS_DEFAULT_FORMAT,
'f' => '8khz_8bit_mono'
]);
if ($response->successful()) {
return $response->body();
}
Log::error('TTS API Error: ' . $response->body());
return null;
}
protected function handleAnnouncementAck(string $message)
{
try {
$data = json_decode($message, true);
if (isset($data['announcement_id'])) {
Announcement::where('id', $data['announcement_id'])
->update(['status' => 'delivered']);
Log::info('Announcement delivered', $data);
}
} catch (\Exception $e) {
Log::error('ACK Handler Error: ' . $e->getMessage());
}
}
protected function handleAnnouncementError(string $message)
{
try {
$data = json_decode($message, true);
if (isset($data['announcement_id'])) {
Announcement::where('id', $data['announcement_id'])
->update([
'status' => 'failed',
'error_message' => $data['error'] ?? 'Unknown error'
]);
Log::error('Announcement failed', $data);
}
} catch (\Exception $e) {
Log::error('Error Handler Error: ' . $e->getMessage());
}
}
protected function handleRelayStatusUpdate(string $message)
{
try {
$data = json_decode($message, true);
if (isset($data['ruangan_id'], $data['state'])) {
Ruangan::where('id', $data['ruangan_id'])
->update(['relay_state' => $data['state']]);
Log::info('Relay status updated', $data);
}
} catch (\Exception $e) {
Log::error('Relay Status Handler Error: ' . $e->getMessage());
}
}
public function ttsPreview(Request $request)
{
$validator = Validator::make($request->all(), [
'text' => 'required|string|max:1000',
'voice' => 'required|string',
'speed' => 'required|integer|min:-10|max:10'
]);
if ($validator->fails()) {
return response()->json([
'error' => $validator->errors()->first()
], 400);
}
try {
$audioContent = $this->generateTTS(
$request->text,
$request->voice,
$request->speed
);
if (!$audioContent) {
throw new \Exception('Failed to generate TTS audio');
}
$fileName = 'tts/previews/' . uniqid() . '.wav';
Storage::disk('public')->put($fileName, $audioContent);
return response()->json([
'audio_url' => asset('storage/' . $fileName)
]);
} catch (\Exception $e) {
Log::error('TTS Preview Error: ' . $e->getMessage());
return response()->json([
'error' => 'Failed to generate preview'
], 500);
}
}
public function history(Request $request)
{
$search = $request->input('search');
$mode = $request->input('mode');
$relayState = $request->input('relay_state');
$announcements = Announcement::with(['ruangans'])
->when($search, function($query) use ($search) {
return $query->where('message', 'like', "%{$search}%")
->orWhereHas('ruangans', function($q) use ($search) {
$q->where('nama_ruangan', 'like', "%{$search}%");
});
})
->when($mode, function($query) use ($mode) {
return $query->where('mode', $mode);
})
->when($relayState, function($query) use ($relayState) {
return $query->where('relay_state', $relayState);
})
->latest()
->paginate(10);
return view('admin.announcement.history', [
'announcements' => $announcements,
'search' => $search,
'mode' => $mode,
'relay_state' => $relayState,
'modes' => [self::MODE_REGULER, self::MODE_TTS],
'relayStates' => [self::RELAY_ON, self::RELAY_OFF]
]);
}
}