Commit 25/09/2025

This commit is contained in:
rdkartono
2025-09-25 11:28:52 +07:00
parent 7db10bd45a
commit e18a08ab6a
3 changed files with 261 additions and 72 deletions

View File

@@ -2,17 +2,20 @@ import audio.AudioPlayer
import barix.BarixConnection
import barix.TCP_Barix_Command_Server
import codes.Somecodes.Companion.Get_ANN_ID
import codes.Somecodes.Companion.IsAlphabethic
import codes.Somecodes.Companion.IsNumber
import codes.Somecodes.Companion.ValidFile
import codes.Somecodes.Companion.ValidString
import codes.Somecodes.Companion.dateformat1
import codes.Somecodes.Companion.timeformat2
import com.sun.jna.Platform
import content.Category
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
@@ -24,18 +27,19 @@ import web.WebApp
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.LocalTime
import java.util.function.Consumer
import kotlin.concurrent.fixedRateTimer
fun main() {
val version = "0.0.1 (23/09/2025)"
val StreamerOutputs : MutableMap<String, BarixConnection> = HashMap()
val StreamerOutputs: MutableMap<String, BarixConnection> = HashMap()
if (Platform.isWindows()) {
// supaya OSHI bisa mendapatkan CPU usage di Windows seperti di Task Manager
GlobalConfig.set(GlobalConfig.OSHI_OS_WINDOWS_CPU_UTILITY, true)
}
Logger.info{"Starting AAS New Generation version $version"}
Logger.info { "Starting AAS New Generation version $version" }
val audioPlayer = AudioPlayer(44100) // 44100 Hz sampling rate
audioPlayer.InitAudio(1)
val content = ContentCache()
@@ -49,19 +53,19 @@ fun main() {
* @return true jika semua valid, false jika ada yang tidak valid
*/
fun AllBroadcastZonesValid(bz: List<String>): Boolean {
if (bz.isNotEmpty()){
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
.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() }
.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 validchannels.size == bz.size
}
return false
}
@@ -72,9 +76,9 @@ fun main() {
* @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() }
if (bz.isNotEmpty()) {
return bz.all { z1 ->
StreamerOutputs.any { sc -> sc.value.channel == z1 && sc.value.isIdle() }
}
}
return false
@@ -96,88 +100,258 @@ fun main() {
/**
* Fungsi untuk ambil messagebank berdasarkan ANN_ID, diurutkan berdasarkan urutan bahasa di urutan_bahasa
* @param id ANN_ID dari messagebank
* @param languages List of language yang diinginkan, default urutan_bahasa
* @return List of Messagebank
*/
fun Get_MessageBank_by_id(id: Int) : ArrayList<Messagebank>{
fun Get_MessageBank_by_id(id: Int, languages: List<String> = urutan_bahasa): 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)
}
languages.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
}
/**
* Find soundbank files from messagebank tags, filtered by VoiceType and Language
* @param mb Messagebank object
* @param variables Map of variables to replace in tags.
* @param cbOK Callback function if success, returns List of soundbank file names
* @param cbFail Callback function if fail, returns error message
*/
fun Get_Soundbank_Files(
mb: Messagebank,
variables: Map<String, String>,
cbOK: Consumer<List<String>>,
cbFail: Consumer<String>
) {
val tags = mb.Message_TAGS.split(" ")
if (tags.isEmpty()) {
cbFail.accept("No tags found in messagebank id ${mb.ANN_ID}")
return
}
// dapatkan soundbank array berdasarkan VoiceType dan Language
val sb = db.SoundbankList
.filter { it.VoiceType == mb.Voice_Type }
.filter { it.Language == mb.Language }
if (sb.isEmpty()) {
cbFail.accept("No soundbank found for voice type ${mb.Voice_Type} and language ${mb.Language}")
return
}
val files = mutableListOf<String>()
tags.forEach { tag ->
val _tag = tag.trim()
when (_tag) {
"[AIRPLANE_NAME]" -> {
}
"[FLIGHT_NUMBER]" -> {
}
"[PLATNOMOR]" -> {
}
"[CITY]" -> {
}
"[PLACES]" -> {
}
"[ETAD]" -> {
}
"[SHALAT]" -> {
}
"[BCB]" -> {
}
"[GATENUMBER]" -> {
// gate number bisa angka saja, misalnya 1,2,3
// atau huruf dan angka, misalnya A1, B2, C3, 1A, 2B, 3C
val value = variables["GATENUMBER"].orEmpty()
if (ValidString(value)) {
val values = value.split(",").map { it.trim() }.filter { ValidString(it) }
if (values.isNotEmpty()){
values.forEach {
if (IsNumber(it)){
// gate number hanya angka
} else {
// gate number gabungan huruf dan angka
val regex = Regex("([A-Z])?(\\d+)([A-Z])?")
}
}
} else {
cbFail.accept("GATENUMBER variable is empty after split for tag $_tag in messagebank id ${mb.ANN_ID}")
return
}
} else {
cbFail.accept("GATENUMBER variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}")
return
}
}
"[REASON]" -> {
val reason = sb.firstOrNull { it.Category == Category.Reason.name && it.TAG == _tag }
if (reason != null) {
if (ValidFile(reason.Path)) {
files.add(reason.Path)
} else {
cbFail.accept("Invalid soundbank file ${reason.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")
return
}
} else {
cbFail.accept("No Reason found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")
return
}
}
"[PROCEDURE]" -> {
val procedure = sb.firstOrNull { it.Category == Category.Procedure.name && it.TAG == _tag }
if (procedure != null) {
if (ValidFile(procedure.Path)) {
files.add(procedure.Path)
} else {
cbFail.accept("Invalid soundbank file ${procedure.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")
return
}
} else {
cbFail.accept("No Procedure found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")
return
}
}
else -> {
// Phrase
val phrase = sb.firstOrNull { it.Category == Category.Phrase.name && it.TAG == _tag }
if (phrase != null) {
if (ValidFile(phrase.Path)) {
files.add(phrase.Path)
} else {
cbFail.accept("Invalid soundbank file ${phrase.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")
return
}
} else {
cbFail.accept("No Phrase found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")
return
}
}
}
}
}
jobloop@ while (isActive) {
delay(1000)
// prioritas 1 , habisin queue paging
for(it in db.Read_Queue_Paging()){
if (it.BroadcastZones.isNotBlank()){
val zz = it.BroadcastZones.split(";")
if (AllBroadcastZonesValid(zz)){
if (AllBroadcastZoneIdle(zz)){
if (it.Source=="PAGING"){
for (qp in db.Read_Queue_Paging()) {
if (qp.BroadcastZones.isNotBlank()) {
val zz = qp.BroadcastZones.split(";")
if (AllBroadcastZonesValid(zz)) {
if (AllBroadcastZoneIdle(zz)) {
if (qp.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)} )
if (ValidFile(qp.Message)) {
val afi = audioPlayer.LoadAudioFile(qp.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}
val logmessage =
"Broadcast started PAGING with Filename '${qp.Message}' to zones: ${qp.BroadcastZones}"
Logger.info { logmessage }
db.Add_Log("AAS", logmessage)
db.Delete_Queue_Paging_by_index(it.index)
db.Delete_Queue_Paging_by_index(qp.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" )
db.Delete_Queue_Paging_by_index(qp.index)
db.Add_Log(
"AAS",
"Cancelled paging message with index ${qp.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 {
// cari tags nya, create content nya, broadcast ke semua zone
} else if (qp.Source == "SHALAT") {
val ann_id = Get_ANN_ID(qp.Message)
if (ann_id > 0) {
// shalat, ambil messagebank berdasarkan ann_id dengan bahasa Indonesia saja
Get_MessageBank_by_id(ann_id, listOf(Language.INDONESIA.name)).let { mblist ->
if (mblist.isNotEmpty()) {
//TODO find soundbank
} else {
// tidak ada messagebank dengan ann_id ini, delete from queue paging
db.Delete_Queue_Paging_by_index(qp.index)
db.Add_Log(
"AAS",
"Cancelled Shalat message with index ${qp.index} due to ANN_ID $ann_id not found in Messagebank"
)
}
}
} else{
} 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" )
db.Delete_Queue_Paging_by_index(qp.index)
db.Add_Log(
"AAS",
"Cancelled Shalat message with index ${qp.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.Delete_Queue_Paging_by_index(qp.index)
db.Add_Log(
"AAS",
"Cancelled paging message with index ${qp.index} due to invalid broadcast zone"
)
}
} else {
// invalid broadcast zone, 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 empty broadcast zone")
db.Delete_Queue_Paging_by_index(qp.index)
db.Add_Log("AAS", "Cancelled paging message with index ${qp.index} due to empty broadcast zone")
}
}
// prioritas 2, habisin queue table
db.Read_Queue_Table().forEach {
if (it.BroadcastZones.isNotEmpty()){
if (it.BroadcastZones.isNotEmpty()) {
val zz = it.BroadcastZones.split(";")
if (AllBroadcastZonesValid(zz)){
if (AllBroadcastZoneIdle(zz)){
if (it.Type=="SOUNDBANK"){
if (AllBroadcastZonesValid(zz)) {
if (AllBroadcastZoneIdle(zz)) {
if (it.Type == "SOUNDBANK") {
} else if (it.Type=="TIMER"){
} 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
@@ -195,10 +369,10 @@ fun main() {
delay(1000)
val localtime = LocalTime.now()
// detik harus 00
if (localtime.second!=0) continue
if (localtime.second != 0) continue
val timestring = timeformat2.format(localtime)
val sch = db.SchedulebankList.filter{
it.Time==timestring && it.Enable
val sch = db.SchedulebankList.filter {
it.Time == timestring && it.Enable
}
// tidak ada schedule dengan time sekarang dan enable=true
if (sch.isEmpty()) continue
@@ -207,15 +381,15 @@ fun main() {
val ddmmyyyy = dateformat1.format(localdate)
// check special date dulu
val specialdate = sch.find {
it.Day==ddmmyyyy
it.Day == ddmmyyyy
}
if (specialdate!=null) {
if (specialdate != null) {
// TODO Masukin ke queue table sebagai schedule special date
}
// cek weekly schedule
val weekly = sch.find {
it.Day == when(localdate.dayOfWeek){
it.Day == when (localdate.dayOfWeek) {
DayOfWeek.MONDAY -> ScheduleDay.Monday.name
DayOfWeek.TUESDAY -> ScheduleDay.Tuesday.name
DayOfWeek.WEDNESDAY -> ScheduleDay.Wednesday.name
@@ -225,7 +399,7 @@ fun main() {
DayOfWeek.SUNDAY -> ScheduleDay.Sunday.name
}
}
if (weekly!=null) {
if (weekly != null) {
// TODO Masukin ke queue table sebagai schedule weekly
}
@@ -233,13 +407,12 @@ fun main() {
val daily = sch.find {
it.Day == ScheduleDay.Everyday.name
}
if (daily!=null) {
if (daily != null) {
// TODO Masukin ke queue table sebagai schedule daily
}
}
}
@@ -252,16 +425,16 @@ fun main() {
)
web.Start()
val barixserver = TCP_Barix_Command_Server ()
val barixserver = TCP_Barix_Command_Server()
barixserver.StartTcpServer { cmd ->
Logger.info{cmd}
Logger.info { cmd }
val _streamer = StreamerOutputs[cmd.ipaddress]
val _sc = db.SoundChannelList.find { it.ip == cmd.ipaddress }
if (_streamer==null){
if (_streamer == null) {
// belum create BarixConnection untuk ipaddress ini
Logger.info{"New Streamer Output connection from ${cmd.ipaddress}"}
if (_sc!=null){
val _bc = BarixConnection(_sc.index,_sc.channel,cmd.ipaddress)
Logger.info { "New Streamer Output connection from ${cmd.ipaddress}" }
if (_sc != null) {
val _bc = BarixConnection(_sc.index, _sc.channel, cmd.ipaddress)
_bc.vu = cmd.vu
_bc.bufferRemain = cmd.buffremain
_bc.statusData = cmd.statusdata
@@ -270,7 +443,7 @@ fun main() {
} else {
// sudah ada, update data
if (_sc !=null && _sc.channel != _streamer.channel) {
if (_sc != null && _sc.channel != _streamer.channel) {
_streamer.channel = _sc.channel
}
_streamer.vu = cmd.vu
@@ -280,7 +453,7 @@ fun main() {
}
val onlinechecker = fixedRateTimer(name="onlinecheck", initialDelay = 1000, period = 1000) {
val onlinechecker = fixedRateTimer(name = "onlinecheck", initialDelay = 1000, period = 1000) {
// cek setiap 1 detik, decrement online counter semua BarixConnection
StreamerOutputs.values.forEach {
it.decrementOnlineCounter()
@@ -289,14 +462,14 @@ fun main() {
// shutdown hook
Runtime.getRuntime().addShutdownHook(Thread {
Logger.info{"Shutdown hook called, stopping services..."}
Logger.info { "Shutdown hook called, stopping services..." }
barixserver.StopTcpCommand()
onlinechecker.cancel()
web.Stop()
audioPlayer.Close()
db.close()
Logger.info{"All services stopped, exiting application."}
} )
Logger.info { "All services stopped, exiting application." }
})
}

View File

@@ -38,6 +38,20 @@ class Somecodes {
// regex for getting ann_id from Message, which is the number inside []
private val ann_id_regex = Regex("\\[(\\d+)]")
/**
* Check if a string is a valid number.
*/
fun IsNumber(value: String) : Boolean {
return value.toIntOrNull() != null
}
/**
* Check if a string is alphabetic (contains only letters).
*/
fun IsAlphabethic(value: String) : Boolean {
return value.all { it.isLetter() }
}
/**
* Extract ANN ID from a message string.
* The ANN ID is expected to be a number enclosed in square brackets (e.g., "[123]").

View File

@@ -11,5 +11,7 @@ enum class Category(name: String) {
PlatNomor("PlatNomor"),
Shalat("Shalat"),
Year("Year"),
Birthday("Birthday");
Birthday("Birthday"),
Reason("Reason"),
Procedure("Procedure");
}