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 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<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)
// baca dulu queue paging, prioritas 1
db.Read_Queue_Paging().forEach {
// cek apakah queue paging ada broadcast zone nya
// 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")
}
}
}
}

View File

@@ -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<String>, cbFail: Consumer<String>) {
if (data.isNotEmpty()) {
CoroutineScope(Dispatchers.IO).launch {
val bb = ByteBuffer.wrap(data)
while(bb.hasRemaining()){
try {
udp.send(DatagramPacket(data, data.size, inet))
return true
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) {
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
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.

View File

@@ -8,7 +8,36 @@ import audio.AudioFileInfo
*/
@Suppress("unused")
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.
@@ -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 &&

View File

@@ -36,6 +36,8 @@ class MariaDB(
var SchedulebankList: ArrayList<ScheduleBank> = ArrayList()
var BroadcastZoneList: ArrayList<BroadcastZones> = ArrayList()
var SoundChannelList: ArrayList<SoundChannel> = ArrayList()
var QueuePagingList: ArrayList<QueuePaging> = ArrayList()
var QueueTableList: ArrayList<QueueTable> = 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<QueueTable> {
QueueTableList.clear()
val queueList = ArrayList<QueueTable>()
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<QueuePaging> {
QueuePagingList.clear()
val queueList = ArrayList<QueuePaging>()
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