commit 24/09/2025

This commit is contained in:
2025-09-24 16:03:07 +07:00
parent 55f6a24cce
commit 1fcf64fd99
5 changed files with 269 additions and 30 deletions

View File

@@ -1,12 +1,18 @@
import audio.AudioPlayer import audio.AudioPlayer
import barix.BarixConnection import barix.BarixConnection
import barix.TCP_Barix_Command_Server 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.dateformat1
import codes.Somecodes.Companion.timeformat2 import codes.Somecodes.Companion.timeformat2
import com.sun.jna.Platform import com.sun.jna.Platform
import content.ContentCache import content.ContentCache
import content.Language
import content.ScheduleDay import content.ScheduleDay
import content.VoiceType
import database.MariaDB import database.MariaDB
import database.Messagebank
import database.QueuePaging
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -34,25 +40,118 @@ fun main() {
audioPlayer.InitAudio(1) audioPlayer.InitAudio(1)
val content = ContentCache() val content = ContentCache()
val db = MariaDB() val db = MariaDB()
// Coroutine untuk cek Paging Queue dan AAS Queue setiap detik // Coroutine untuk cek Paging Queue dan AAS Queue setiap detik
CoroutineScope(Dispatchers.Default).launch { 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<String>): 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<String>): 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<Messagebank>{
val mb_list = ArrayList<Messagebank>()
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) delay(1000)
// baca dulu queue paging, prioritas 1 // prioritas 1 , habisin queue paging
db.Read_Queue_Paging().forEach { for(it in db.Read_Queue_Paging()){
// cek apakah queue paging ada broadcast zone nya if (it.BroadcastZones.isNotBlank()){
if (it.BroadcastZones.isNotBlank()) {
val zz = it.BroadcastZones.split(";") val zz = it.BroadcastZones.split(";")
// cek apakah semua target broadcast zone dari queue paging ada di dalam database broadcast zones if (AllBroadcastZonesValid(zz)){
if (zz.all { z -> db.BroadcastZoneList.any { bz -> bz.equals(z) } }) { if (AllBroadcastZoneIdle(zz)){
// semua target broadcast zone valid, sekarang cek apakah semua target broadcast zone idle 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 { } else {
// ada broadcast zone yang tidak valid, delete from queue paging // ada broadcast zone yang tidak valid, delete from queue paging
db.Delete_Queue_Paging_by_index(it.index) 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 { } else {
// invalid broadcast zone, delete from queue paging // 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") 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 { } else {
// ada broadcast zone yang tidak valid, delete from queue table // ada broadcast zone yang tidak valid, delete from queue table
db.Delete_Queue_Table_by_index(it.index) 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 { } else {
// invalid broadcast zone, delete from queue table // 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") db.Add_Log("AAS", "Cancelled table message with index ${it.index} due to empty broadcast zone")
} }
} }
} }
} }

View File

@@ -2,10 +2,15 @@ package barix
import codes.Somecodes import codes.Somecodes
import com.fasterxml.jackson.databind.JsonNode 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.DatagramPacket
import java.net.DatagramSocket import java.net.DatagramSocket
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.nio.ByteBuffer
import java.util.function.Consumer
@Suppress("unused") @Suppress("unused")
class BarixConnection(val index: UInt, var channel: String, val ipaddress: String, val port: Int = 5002) { 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 { fun isOnline(): Boolean {
return _onlinecounter > 0 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 * Send data to Barix device via UDP
* @param data The data to send * @param data The data to send
* @return true if successful
*/ */
fun SendData(data: ByteArray): Boolean { fun SendData(data: ByteArray, cbOK: Consumer<String>, cbFail: Consumer<String>) {
if (data.isNotEmpty()) { if (data.isNotEmpty()) {
CoroutineScope(Dispatchers.IO).launch {
val bb = ByteBuffer.wrap(data)
while(bb.hasRemaining()){
try { try {
udp.send(DatagramPacket(data, data.size, inet)) val chunk = ByteArray(if (bb.remaining() > 1400) 1400 else bb.remaining())
return true bb.get(chunk)
udp.send(DatagramPacket(chunk, chunk.size, inet))
delay(5)
} catch (e: Exception) { } catch (e: Exception) {
Logger.error { "SendData to ${ipaddress}:${port} failed, message: ${e.message}" } cbFail.accept("SendData to $ipaddress:$port failed, message: ${e.message}")
return@launch
} }
} }
return false cbOK.accept("SendData to $channel ($ipaddress:$port) succeeded, ${data.size} bytes sent")
}
}
} }

View File

@@ -35,6 +35,20 @@ class Somecodes {
const val TB_threshold = GB_threshold * 1024.0 const val TB_threshold = GB_threshold * 1024.0
val objectmapper = jacksonObjectMapper() 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. * Convert an object to a JSON string.
* @param data The object to convert. * @param data The object to convert.

View File

@@ -8,7 +8,36 @@ import audio.AudioFileInfo
*/ */
@Suppress("unused") @Suppress("unused")
class ContentCache { class ContentCache {
val contentList = ArrayList<SoundbankData>() private val contentList = ArrayList<SoundbankData>()
/**
* 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. * 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? { fun Get(tag: String, category: Category, language: Language, voiceType: VoiceType): SoundbankData? {
return contentList.find { it -> return contentList.find {
it.TAG == tag && it.TAG == tag &&
it.Category == category && it.Category == category &&
it.Language == language && it.Language == language &&

View File

@@ -36,6 +36,8 @@ class MariaDB(
var SchedulebankList: ArrayList<ScheduleBank> = ArrayList() var SchedulebankList: ArrayList<ScheduleBank> = ArrayList()
var BroadcastZoneList: ArrayList<BroadcastZones> = ArrayList() var BroadcastZoneList: ArrayList<BroadcastZones> = ArrayList()
var SoundChannelList: ArrayList<SoundChannel> = ArrayList() var SoundChannelList: ArrayList<SoundChannel> = ArrayList()
var QueuePagingList: ArrayList<QueuePaging> = ArrayList()
var QueueTableList: ArrayList<QueueTable> = ArrayList()
companion object { companion object {
fun ValidDate(date: String): Boolean { fun ValidDate(date: String): Boolean {
@@ -1578,6 +1580,7 @@ class MariaDB(
* @return A list of QueueTable entries. * @return A list of QueueTable entries.
*/ */
fun Read_Queue_Table(): ArrayList<QueueTable> { fun Read_Queue_Table(): ArrayList<QueueTable> {
QueueTableList.clear()
val queueList = ArrayList<QueueTable>() val queueList = ArrayList<QueueTable>()
try { try {
val statement = connection?.createStatement() val statement = connection?.createStatement()
@@ -1595,6 +1598,7 @@ class MariaDB(
resultSet.getString("Language") resultSet.getString("Language")
) )
queueList.add(queueTable) queueList.add(queueTable)
QueueTableList.add(queueTable)
} }
} catch (e: Exception) { } catch (e: Exception) {
Logger.error("Error fetching queue table: ${e.message}" as Any) Logger.error("Error fetching queue table: ${e.message}" as Any)
@@ -1614,6 +1618,7 @@ class MariaDB(
val rowsAffected = statement?.executeUpdate() val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) { if (rowsAffected != null && rowsAffected > 0) {
Logger.info("Deleted $rowsAffected row(s) from queue_table with index $index" as Any) Logger.info("Deleted $rowsAffected row(s) from queue_table with index $index" as Any)
Resort_Queue_Table_by_Index()
return true return true
} else { } else {
Logger.warn("No rows deleted from queue_table with index $index" as Any) Logger.warn("No rows deleted from queue_table with index $index" as Any)
@@ -1624,10 +1629,34 @@ class MariaDB(
return false 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. * Clears all entries from the queue_table in the database.
*/ */
fun Clear_Queue_Table(): Boolean { fun Clear_Queue_Table(): Boolean {
QueueTableList.clear()
try { try {
val statement = connection?.createStatement() val statement = connection?.createStatement()
// use TRUNCATE to reset auto increment index // use TRUNCATE to reset auto increment index
@@ -1645,6 +1674,7 @@ class MariaDB(
* @return A list of QueuePaging entries. * @return A list of QueuePaging entries.
*/ */
fun Read_Queue_Paging(): ArrayList<QueuePaging> { fun Read_Queue_Paging(): ArrayList<QueuePaging> {
QueuePagingList.clear()
val queueList = ArrayList<QueuePaging>() val queueList = ArrayList<QueuePaging>()
try { try {
val statement = connection?.createStatement() val statement = connection?.createStatement()
@@ -1659,6 +1689,7 @@ class MariaDB(
resultSet.getString("SB_TAGS"), resultSet.getString("SB_TAGS"),
) )
queueList.add(queuePaging) queueList.add(queuePaging)
QueuePagingList.add(queuePaging)
} }
} catch (e: Exception) { } catch (e: Exception) {
Logger.error("Error fetching queue paging: ${e.message}" as Any) Logger.error("Error fetching queue paging: ${e.message}" as Any)
@@ -1678,6 +1709,7 @@ class MariaDB(
val rowsAffected = statement?.executeUpdate() val rowsAffected = statement?.executeUpdate()
if (rowsAffected != null && rowsAffected > 0) { if (rowsAffected != null && rowsAffected > 0) {
Logger.info("Deleted $rowsAffected row(s) from queue_paging with index $index" as Any) Logger.info("Deleted $rowsAffected row(s) from queue_paging with index $index" as Any)
Resort_Queue_Paging_by_Index()
return true return true
} else { } else {
Logger.warn("No rows deleted from queue_paging with index $index" as Any) Logger.warn("No rows deleted from queue_paging with index $index" as Any)
@@ -1688,10 +1720,34 @@ class MariaDB(
return false 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. * Clears all entries from the queue_paging in the database.
*/ */
fun Clear_Queue_Paging(): Boolean { fun Clear_Queue_Paging(): Boolean {
QueuePagingList.clear()
try { try {
val statement = connection?.createStatement() val statement = connection?.createStatement()
// use TRUNCATE to reset auto increment index // use TRUNCATE to reset auto increment index