From 1fcf64fd994c22e46d8dc222b38a24ffcac74e3e Mon Sep 17 00:00:00 2001 From: rdkartono Date: Wed, 24 Sep 2025 16:03:07 +0700 Subject: [PATCH] commit 24/09/2025 --- src/Main.kt | 144 ++++++++++++++++++++++++++++++----- src/barix/BarixConnection.kt | 52 ++++++++++--- src/codes/Somecodes.kt | 14 ++++ src/content/ContentCache.kt | 33 +++++++- src/database/MariaDB.kt | 56 ++++++++++++++ 5 files changed, 269 insertions(+), 30 deletions(-) diff --git a/src/Main.kt b/src/Main.kt index de6fb91..71e1c5d 100644 --- a/src/Main.kt +++ b/src/Main.kt @@ -1,12 +1,18 @@ import audio.AudioPlayer import barix.BarixConnection import barix.TCP_Barix_Command_Server +import codes.Somecodes.Companion.Get_ANN_ID +import codes.Somecodes.Companion.ValidFile import codes.Somecodes.Companion.dateformat1 import codes.Somecodes.Companion.timeformat2 import com.sun.jna.Platform import content.ContentCache +import content.Language import content.ScheduleDay +import content.VoiceType import database.MariaDB +import database.Messagebank +import database.QueuePaging import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -34,25 +40,118 @@ fun main() { audioPlayer.InitAudio(1) val content = ContentCache() val db = MariaDB() + // Coroutine untuk cek Paging Queue dan AAS Queue setiap detik CoroutineScope(Dispatchers.Default).launch { - while (isActive) { + /** + * Fungsi untuk cek apakah semua broadcast zone valid + * @param bz List of broadcast zone (SoundChannel) + * @return true jika semua valid, false jika ada yang tidak valid + */ + fun AllBroadcastZonesValid(bz: List): Boolean { + if (bz.isNotEmpty()){ + val validchannels = bz + // check apakah tiap zone ada di database broadcast zones + .filter { + z1 -> db.BroadcastZoneList.find { z2 -> z2.SoundChannel==z1 } != null + } + // check apakah tiap zone ada di SoundChannelList dan Online + .filter { + z3 -> StreamerOutputs.any { sc -> sc.value.channel==z3 && sc.value.isOnline() } + } + + // kalau jumlah valid channel sama dengan jumlah broadcast zone, berarti semua valid + return validchannels.size==bz.size + } + return false + } + + /** + * Fungsi untuk cek apakah semua broadcast zone idle + * @param bz List of broadcast zone (SoundChannel) + * @return true jika semua idle, false jika ada yang tidak idle + */ + fun AllBroadcastZoneIdle(bz: List): Boolean { + if (bz.isNotEmpty()){ + return bz.all { + z1 -> StreamerOutputs.any { sc -> sc.value.channel==z1 && sc.value.isIdle() } + } + } + return false + } + + // dipakai untuk ambil messagebank berdasarkan id + val urutan_bahasa = listOf( + Language.INDONESIA.name, + Language.LOCAL.name, + Language.ENGLISH.name, + Language.CHINESE.name, + Language.JAPANESE.name, + Language.ARABIC.name + ) + + // dipakai untuk pilih voice type, bisa diganti via web nanti + var selected_voice = VoiceType.VOICE_1.name + + /** + * Fungsi untuk ambil messagebank berdasarkan ANN_ID, diurutkan berdasarkan urutan bahasa di urutan_bahasa + * @param id ANN_ID dari messagebank + * @return List of Messagebank + */ + fun Get_MessageBank_by_id(id: Int) : ArrayList{ + val mb_list = ArrayList() + urutan_bahasa.forEach { + lang -> db.MessagebankList.find { mb -> mb.ANN_ID==id.toUInt() && mb.Language==lang && mb.Voice_Type==selected_voice }?.let { + mb_list.add(it) + } + } + return mb_list + } + + jobloop@ while (isActive) { delay(1000) - // baca dulu queue paging, prioritas 1 - db.Read_Queue_Paging().forEach { - // cek apakah queue paging ada broadcast zone nya - if (it.BroadcastZones.isNotBlank()) { + // prioritas 1 , habisin queue paging + for(it in db.Read_Queue_Paging()){ + if (it.BroadcastZones.isNotBlank()){ val zz = it.BroadcastZones.split(";") - // cek apakah semua target broadcast zone dari queue paging ada di dalam database broadcast zones - if (zz.all { z -> db.BroadcastZoneList.any { bz -> bz.equals(z) } }) { - // semua target broadcast zone valid, sekarang cek apakah semua target broadcast zone idle + if (AllBroadcastZonesValid(zz)){ + if (AllBroadcastZoneIdle(zz)){ + if (it.Source=="PAGING"){ + // nama file ada di Message + if (ValidFile(it.Message)){ + val afi = audioPlayer.LoadAudioFile(it.Message) + zz.forEach { + z1 -> StreamerOutputs.values.find { it.channel==z1 }?.SendData(afi.bytes, {db.Add_Log("AAS", it) }, {db.Add_Log("AAS", it)} ) + } + val logmessage = "Broadcast started PAGING with Filename '${it.Message}' to zones: ${it.BroadcastZones}" + Logger.info { logmessage} + db.Add_Log("AAS", logmessage) + db.Delete_Queue_Paging_by_index(it.index) + continue@jobloop + } else { + // file tidak valid, delete from queue paging + db.Delete_Queue_Paging_by_index(it.index) + db.Add_Log("AAS", "Cancelled paging message with index ${it.index} due to invalid audio file" ) + } + } else if (it.Source=="SHALAT"){ + val ann_id = Get_ANN_ID(it.Message) + if (ann_id>0){ + Get_MessageBank_by_id(ann_id).forEach { + + } + } else{ + // invalid ann_id, delete from queue paging + db.Delete_Queue_Paging_by_index(it.index) + db.Add_Log("AAS", "Cancelled Shalat message with index ${it.index} due to invalid ANN_ID" ) + } + } + } } else { // ada broadcast zone yang tidak valid, delete from queue paging db.Delete_Queue_Paging_by_index(it.index) - db.Add_Log("AAS", "Cancelled paging message with index ${it.index} due to invalid broadcast zone" - ) + db.Add_Log("AAS", "Cancelled paging message with index ${it.index} due to invalid broadcast zone" ) } } else { // invalid broadcast zone, delete from queue paging @@ -60,19 +159,25 @@ fun main() { db.Add_Log("AAS", "Cancelled paging message with index ${it.index} due to empty broadcast zone") } } - // baca kemudian queue table, prioritas 2 - db.Read_Queue_Table().forEach { - if (it.BroadcastZones.isNotBlank()) { - val zz = it.BroadcastZones.split(";") - // cek apakah semua target broadcast zone dari queue table ada di dalam database broadcast zones - if (zz.all { z -> db.BroadcastZoneList.any { bz -> bz.equals(z) } }) { - // semua target broadcast zone valid, sekarang cek apakah semua target broadcast zone idle + + // prioritas 2, habisin queue table + db.Read_Queue_Table().forEach { + if (it.BroadcastZones.isNotEmpty()){ + val zz = it.BroadcastZones.split(";") + if (AllBroadcastZonesValid(zz)){ + if (AllBroadcastZoneIdle(zz)){ + if (it.Type=="SOUNDBANK"){ + + } else if (it.Type=="TIMER"){ + + } + + } } else { // ada broadcast zone yang tidak valid, delete from queue table db.Delete_Queue_Table_by_index(it.index) - db.Add_Log("AAS", "Cancelled table message with index ${it.index} due to invalid broadcast zone" - ) + db.Add_Log("AAS", "Cancelled table message with index ${it.index} due to invalid broadcast zone") } } else { // invalid broadcast zone, delete from queue table @@ -80,6 +185,7 @@ fun main() { db.Add_Log("AAS", "Cancelled table message with index ${it.index} due to empty broadcast zone") } } + } } diff --git a/src/barix/BarixConnection.kt b/src/barix/BarixConnection.kt index 31036fe..18789f4 100644 --- a/src/barix/BarixConnection.kt +++ b/src/barix/BarixConnection.kt @@ -2,10 +2,15 @@ package barix import codes.Somecodes import com.fasterxml.jackson.databind.JsonNode -import org.tinylog.Logger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import java.net.DatagramPacket import java.net.DatagramSocket import java.net.InetSocketAddress +import java.nio.ByteBuffer +import java.util.function.Consumer @Suppress("unused") class BarixConnection(val index: UInt, var channel: String, val ipaddress: String, val port: Int = 5002) { @@ -57,25 +62,54 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin } } + /** + * Check if Barix device is online + * @return true if online + */ fun isOnline(): Boolean { return _onlinecounter > 0 } + /** + * Check if Barix device is idle (not playing) + * @return true if idle + */ + fun isIdle() : Boolean{ + return statusData == 0 + } + + /** + * Check if Barix device is playing + * @return true if playing + */ + fun isPlaying() : Boolean{ + return statusData == 1 + } + /** * Send data to Barix device via UDP * @param data The data to send - * @return true if successful */ - fun SendData(data: ByteArray): Boolean { + fun SendData(data: ByteArray, cbOK: Consumer, cbFail: Consumer) { if (data.isNotEmpty()) { - try { - udp.send(DatagramPacket(data, data.size, inet)) - return true - } catch (e: Exception) { - Logger.error { "SendData to ${ipaddress}:${port} failed, message: ${e.message}" } + CoroutineScope(Dispatchers.IO).launch { + val bb = ByteBuffer.wrap(data) + while(bb.hasRemaining()){ + try { + val chunk = ByteArray(if (bb.remaining() > 1400) 1400 else bb.remaining()) + bb.get(chunk) + udp.send(DatagramPacket(chunk, chunk.size, inet)) + delay(5) + } catch (e: Exception) { + cbFail.accept("SendData to $ipaddress:$port failed, message: ${e.message}") + return@launch + } + } + cbOK.accept("SendData to $channel ($ipaddress:$port) succeeded, ${data.size} bytes sent") + } + } - return false } diff --git a/src/codes/Somecodes.kt b/src/codes/Somecodes.kt index fbb5399..7b84e00 100644 --- a/src/codes/Somecodes.kt +++ b/src/codes/Somecodes.kt @@ -35,6 +35,20 @@ class Somecodes { const val TB_threshold = GB_threshold * 1024.0 val objectmapper = jacksonObjectMapper() + // regex for getting ann_id from Message, which is the number inside [] + private val ann_id_regex = Regex("\\[(\\d+)]") + + /** + * Extract ANN ID from a message string. + * The ANN ID is expected to be a number enclosed in square brackets (e.g., "[123]"). + * @param message The message string to extract the ANN ID from. + * @return The extracted ANN ID as an integer, or -1 if not found or invalid. + */ + fun Get_ANN_ID(message: String) : Int { + val matchResult = ann_id_regex.find(message) + return matchResult?.groups?.get(1)?.value?.toInt() ?: -1 + } + /** * Convert an object to a JSON string. * @param data The object to convert. diff --git a/src/content/ContentCache.kt b/src/content/ContentCache.kt index 5e3e0e7..ecffe21 100644 --- a/src/content/ContentCache.kt +++ b/src/content/ContentCache.kt @@ -8,7 +8,36 @@ import audio.AudioFileInfo */ @Suppress("unused") class ContentCache { - val contentList = ArrayList() + private val contentList = ArrayList() + + + + /** + * Clears all loaded content from the cache. + */ + fun Clear(){ + contentList.clear() + } + + /** + * Removes the specified SoundbankData from the content list. + * @param SoundbankData The SoundbankData to be removed. + */ + fun Remove(SoundbankData: SoundbankData){ + contentList.remove(SoundbankData) + } + + /** + * Removes the specified SoundbankData from the content list based on tag, category, language, and voiceType. + * @param tag The tag of the SoundbankData to be removed. + * @param category The category of the SoundbankData to be removed. + * @param language The language of the SoundbankData to be removed. + * @param voiceType The voice type of the SoundbankData to be removed. + */ + fun Remove(tag: String, category: Category, language: Language, voiceType: VoiceType){ + val existing = Get(tag, category, language, voiceType) + if (existing!=null) contentList.remove(existing) + } /** * Get the specified SoundbankData from tag, category, language, and voiceType. @@ -16,7 +45,7 @@ class ContentCache { */ fun Get(tag: String, category: Category, language: Language, voiceType: VoiceType): SoundbankData? { - return contentList.find { it -> + return contentList.find { it.TAG == tag && it.Category == category && it.Language == language && diff --git a/src/database/MariaDB.kt b/src/database/MariaDB.kt index a40d208..36e705f 100644 --- a/src/database/MariaDB.kt +++ b/src/database/MariaDB.kt @@ -36,6 +36,8 @@ class MariaDB( var SchedulebankList: ArrayList = ArrayList() var BroadcastZoneList: ArrayList = ArrayList() var SoundChannelList: ArrayList = ArrayList() + var QueuePagingList: ArrayList = ArrayList() + var QueueTableList: ArrayList = ArrayList() companion object { fun ValidDate(date: String): Boolean { @@ -1578,6 +1580,7 @@ class MariaDB( * @return A list of QueueTable entries. */ fun Read_Queue_Table(): ArrayList { + QueueTableList.clear() val queueList = ArrayList() try { val statement = connection?.createStatement() @@ -1595,6 +1598,7 @@ class MariaDB( resultSet.getString("Language") ) queueList.add(queueTable) + QueueTableList.add(queueTable) } } catch (e: Exception) { Logger.error("Error fetching queue table: ${e.message}" as Any) @@ -1614,6 +1618,7 @@ class MariaDB( val rowsAffected = statement?.executeUpdate() if (rowsAffected != null && rowsAffected > 0) { Logger.info("Deleted $rowsAffected row(s) from queue_table with index $index" as Any) + Resort_Queue_Table_by_Index() return true } else { Logger.warn("No rows deleted from queue_table with index $index" as Any) @@ -1624,10 +1629,34 @@ class MariaDB( return false } + /** + * Resort the queue_table table, in order to reorder the index after deletions, and sort ascending by index. + * @return True if the resorting was successful, false otherwise. + */ + fun Resort_Queue_Table_by_Index(): Boolean { + try { + val statement = connection?.createStatement() + // use a temporary table to reorder the index + statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_queue_table LIKE queue_table") + statement?.executeUpdate("INSERT INTO temp_queue_table (Date_Time, Source, Type, Message, SB_TAGS, BroadcastZones, Repeat, Language) SELECT Date_Time, Source, Type, Message, SB_TAGS, BroadcastZones, Repeat, Language FROM queue_table ORDER BY `index` ASC") + statement?.executeUpdate("TRUNCATE TABLE queue_table") + statement?.executeUpdate("INSERT INTO queue_table (Date_Time, Source, Type, Message, SB_TAGS, BroadcastZones, Repeat, Language) SELECT Date_Time, Source, Type, Message, SB_TAGS, BroadcastZones, Repeat, Language FROM temp_queue_table") + statement?.executeUpdate("DROP TABLE temp_queue_table") + Logger.info("queue_table table resorted by index" as Any) + // reload the local list + Read_Queue_Table() + return true + } catch (e: Exception) { + Logger.error("Error resorting queue_table table by index: ${e.message}" as Any) + } + return false + } + /** * Clears all entries from the queue_table in the database. */ fun Clear_Queue_Table(): Boolean { + QueueTableList.clear() try { val statement = connection?.createStatement() // use TRUNCATE to reset auto increment index @@ -1645,6 +1674,7 @@ class MariaDB( * @return A list of QueuePaging entries. */ fun Read_Queue_Paging(): ArrayList { + QueuePagingList.clear() val queueList = ArrayList() try { val statement = connection?.createStatement() @@ -1659,6 +1689,7 @@ class MariaDB( resultSet.getString("SB_TAGS"), ) queueList.add(queuePaging) + QueuePagingList.add(queuePaging) } } catch (e: Exception) { Logger.error("Error fetching queue paging: ${e.message}" as Any) @@ -1678,6 +1709,7 @@ class MariaDB( val rowsAffected = statement?.executeUpdate() if (rowsAffected != null && rowsAffected > 0) { Logger.info("Deleted $rowsAffected row(s) from queue_paging with index $index" as Any) + Resort_Queue_Paging_by_Index() return true } else { Logger.warn("No rows deleted from queue_paging with index $index" as Any) @@ -1688,10 +1720,34 @@ class MariaDB( return false } + /** + * Resort the queue_paging table, in order to reorder the index after deletions, and sort ascending by index. + * @return True if the resorting was successful, false otherwise. + */ + fun Resort_Queue_Paging_by_Index(): Boolean { + try { + val statement = connection?.createStatement() + // use a temporary table to reorder the index + statement?.executeUpdate("CREATE TABLE IF NOT EXISTS temp_queue_paging LIKE queue_paging") + statement?.executeUpdate("INSERT INTO temp_queue_paging (Date_Time, Source, Type, Message, SB_TAGS) SELECT Date_Time, Source, Type, Message, SB_TAGS FROM queue_paging ORDER BY `index` ASC") + statement?.executeUpdate("TRUNCATE TABLE queue_paging") + statement?.executeUpdate("INSERT INTO queue_paging (Date_Time, Source, Type, Message, SB_TAGS) SELECT Date_Time, Source, Type, Message, SB_TAGS FROM temp_queue_paging") + statement?.executeUpdate("DROP TABLE temp_queue_paging") + Logger.info("queue_paging table resorted by index" as Any) + // reload the local list + Read_Queue_Paging() + return true + } catch (e: Exception) { + Logger.error("Error resorting queue_paging table by index: ${e.message}" as Any) + } + return false + } + /** * Clears all entries from the queue_paging in the database. */ fun Clear_Queue_Paging(): Boolean { + QueuePagingList.clear() try { val statement = connection?.createStatement() // use TRUNCATE to reset auto increment index