From cf24c06b35e16fb15c76e18ee22a86fc8bda66b8 Mon Sep 17 00:00:00 2001 From: rdkartono Date: Mon, 29 Sep 2025 11:56:08 +0700 Subject: [PATCH] Commit 29/09/2025 --- src/Main.kt | 865 +----------------- src/MainExtension01.kt | 838 +++++++++++++++++ .../TCP_Android_Command_Server.kt | 100 +- src/database/MariaDB.kt | 21 + src/database/dbFunctions.kt | 20 + src/web/WebApp.kt | 5 +- 6 files changed, 1004 insertions(+), 845 deletions(-) create mode 100644 src/MainExtension01.kt create mode 100644 src/database/dbFunctions.kt diff --git a/src/Main.kt b/src/Main.kt index 5c229ef..f1dc4f0 100644 --- a/src/Main.kt +++ b/src/Main.kt @@ -1,24 +1,11 @@ -import audio.AudioFileInfo 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.Make_WAV_FileName -import codes.Somecodes.Companion.SoundbankResult_directory -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 commandServer.TCP_Android_Command_Server import content.Language -import content.ScheduleDay import content.VoiceType import database.MariaDB -import database.Messagebank -import database.Soundbank import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -27,842 +14,54 @@ import kotlinx.coroutines.launch import org.tinylog.Logger import oshi.util.GlobalConfig import web.WebApp -import java.time.DayOfWeek -import java.time.LocalDate -import java.time.LocalTime -import java.util.function.Consumer import kotlin.concurrent.fixedRateTimer +lateinit var db: MariaDB +lateinit var audioPlayer: AudioPlayer +val StreamerOutputs: MutableMap = HashMap() +const val version = "0.0.1 (23/09/2025)" +// dipakai untuk pilih voice type, bisa diganti via web nanti +var selected_voice = VoiceType.VOICE_1.name + +// 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 +) + +// Application start here fun main() { - val version = "0.0.1 (23/09/2025)" - val StreamerOutputs: MutableMap = 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" } - val audioPlayer = AudioPlayer(44100) // 44100 Hz sampling rate + audioPlayer = AudioPlayer(44100) // 44100 Hz sampling rate audioPlayer.InitAudio(1) - val db = MariaDB() + db = MariaDB() + + val subcode01 = MainExtension01() // Coroutine untuk cek Paging Queue dan AAS Queue setiap detik CoroutineScope(Dispatchers.Default).launch { - /** - * 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 - * @param languages List of language yang diinginkan, default urutan_bahasa - * @return List of Messagebank - */ - fun Get_MessageBank_by_id(id: Int, languages: List = urutan_bahasa): ArrayList { - val mb_list = ArrayList() - 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 path for AlphabetNumeric category based on value - * @param sb List of Soundbank to search - * @param value String value to search, can be combination of letters and numbers, e.g. A1, B2, 3C, 12, etc. - * @return Soundbank path if found and valid, null if not found or invalid - */ - fun Get_Soundbank_AlpabethNumeric(sb: List, value: String): List? { - val result = mutableListOf() - if (ValidString(value)) { - if (sb.isNotEmpty()) { - val regex = Regex("([A-Z])?(\\d+)([A-Z])?") - val match = regex.find(value) - match?.groupValues?.forEach { - if (IsNumber(it)) { - val num = - sb.firstOrNull { s1 -> s1.Category == Category.AlphabetNumeric.name && s1.TAG == "N$it" } - if (num != null) { - if (ValidFile(num.Path)) { - result.add(num.Path) - } - } - } else if (IsAlphabethic(it)) { - val alp = - sb.firstOrNull { s1 -> s1.Category == Category.AlphabetNumeric.name && s1.TAG == it } - if (alp != null) { - if (ValidFile(alp.Path)) { - result.add(alp.Path) - } - } - } - } - if (result.isNotEmpty()) { - return result - } - } - } - return null - } - - val SoundbankKeywords = listOf("ANN_ID","AL","FLNUM","A_D","I_D","ORIGIN","CITY","SHALAT","PLACES","DESTINATION","ETAD","STANDCODE","GATECODE","REMARK","BCB","PLATNOMOR","REASON","PROCEDURE") - - /** - * Parse soundbank data from string value in format "KEY:VALUE KEY:VALUE ..." - * @param value String value to parse - * @return Map of key-value pairs if valid, null if invalid - */ - fun Get_Soundbank_Data(value: String) : Map? { - if (ValidString(value)){ - val values = value.split(" ").map { it.trim() }.filter { ValidString(it) } - if (values.isNotEmpty()){ - val result = mutableMapOf() - values.forEach { - val kv = it.split(":") - if (kv.size==2){ - val key = kv[0].trim().uppercase() - val val1 = kv[1].trim().uppercase() - if (ValidString(key) && ValidString(val1)){ - if (SoundbankKeywords.contains(key)) result[key] = val1 - } - } - } - if (result.isNotEmpty()) return result - } - } - return null - } - - /** - * 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 failed, returns error message - */ - fun Get_Soundbank_Files( - mb: Messagebank, - variables: Map, - cbOK: Consumer>, - cbFail: Consumer - ) { - 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() - - tags.forEach { tag -> - when (val _tag = tag.trim()) { - "[AIRPLANE_NAME]" -> { - val value = variables["AIRPLANE_NAME"].orEmpty() - if (ValidString(value)) { - val airplane = - sb.firstOrNull { it.Category == Category.Airplane_Name.name && it.TAG == value } - if (airplane != null) { - if (ValidFile(airplane.Path)) { - files.add(airplane.Path) - } else { - cbFail.accept("Invalid soundbank file ${airplane.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") - return - } - } else { - cbFail.accept("No Airplane_Name found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") - return - } - } else { - cbFail.accept("AIRPLANE_NAME variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}") - return - } - - } - - "[FLIGHT_NUMBER]" -> { - val alcode = variables["AIRPLANE_NAME"].orEmpty() - val fncode = variables["FLIGHT_NUMBER"].orEmpty() - if (ValidString(alcode) && ValidString(fncode)) { - val val1 = sb.firstOrNull { it.Category == Category.Airline_Code.name && it.TAG == alcode } - val val2 = Get_Soundbank_AlpabethNumeric(sb, fncode) - if (val1 != null) { - if (ValidFile(val1.Path)) { - files.add(val1.Path) - } else { - cbFail.accept("Invalid soundbank file ${val1.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") - return - } - } else { - cbFail.accept("No Airline_Code found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") - return - } - if (val2 != null && val2.isNotEmpty()) { - files.addAll(val2) - } else { - cbFail.accept("No valid soundbank files found for FLIGHT_NUMBER value '$fncode' for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") - return - } - - } else { - cbFail.accept("AIRPLANE_NAME or FLIGHT_NUMBER variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}") - return - } - - } - - "[PLATNOMOR]" -> { - // plat nomor bisa huruf dan angka, atau huruf angka huruf, misalnya B123CD, AB1234EF, RI1 - val value = variables["PLATNOMOR"].orEmpty() - if (ValidString(value)) { - val regex = Regex("([A-Z]+)(\\d+)([A-Z]*)") - val match = regex.find(value) - if (match != null) { - val depan = match.groups[1]?.value // huruf depan - val tengah = match.groups[2]?.value // angka - val belakang = match.groups[3]?.value // huruf belakang, bisa kosong - // ambilin per huruf - depan?.forEach { - val dep = Get_Soundbank_AlpabethNumeric(sb, it.toString()) - if (dep != null) { - files.addAll(dep) - } - } - // ambilin per angka - tengah?.forEach { - val tgh = Get_Soundbank_AlpabethNumeric(sb, it.toString()) - if (tgh != null) { - files.addAll(tgh) - } - } - // ambilin per huruf - belakang?.forEach { - val blk = Get_Soundbank_AlpabethNumeric(sb, it.toString()) - if (blk != null) { - files.addAll(blk) - } - } - } else { - cbFail.accept("PLATNOMOR variable has invalid format for value '$value' in messagebank id ${mb.ANN_ID}") - return - } - } else { - cbFail.accept("PLATNOMOR variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}") - return - } - } - - "[CITY]" -> { - val values = variables["CITY"].orEmpty().split(";").map { it.trim() }.filter { ValidString(it) } - if (values.isNotEmpty()) { - values.forEach { vv -> - val city = sb.firstOrNull { it.Category == Category.City.name && it.TAG == vv } - if (city != null) { - if (ValidFile(city.Path)) { - files.add(city.Path) - } else { - cbFail.accept("Invalid soundbank file ${city.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") - return - } - } else { - cbFail.accept("No City found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") - return - } - } - } else { - cbFail.accept("CITY variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}") - return - } - } - - "[PLACES]" -> { - val value = variables["PLACES"].orEmpty() - if (ValidString(value)) { - val places = sb.firstOrNull { it.Category == Category.Places.name && it.TAG == value } - if (places != null) { - if (ValidFile(places.Path)) { - files.add(places.Path) - } else { - cbFail.accept("Invalid soundbank file ${places.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") - return - } - } else { - cbFail.accept("No Places found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") - return - } - } else { - cbFail.accept("PLACES variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}") - return - } - - } - - "[ETAD]" -> { - val values = variables["ETAD"].orEmpty().split(":").map { it.trim() }.filter { IsNumber(it) } - if (values.size == 2) { - val hh = Get_Soundbank_AlpabethNumeric(sb, values[0]) - val mm = Get_Soundbank_AlpabethNumeric(sb, values[1]) - if (hh != null && mm != null && hh.isNotEmpty() && mm.isNotEmpty()) { - if (ValidFile(hh[0]) && ValidFile(mm[0])) { - files.add(hh[0]) - files.add(mm[0]) - } else { - cbFail.accept("ETAD variable has invalid soundbank files for tag $_tag in messagebank id ${mb.ANN_ID}, unable to find valid soundbank files for HH='${values[0]}' or MM='${values[1]}'") - return - } - } else { - cbFail.accept("ETAD variable has invalid soundbank for tag $_tag in messagebank id ${mb.ANN_ID}, unable to find soundbank for HH='${values[0]}' or MM='${values[1]}'") - return - } - } else { - cbFail.accept("ETAD variable has invalid format for tag $_tag in messagebank id ${mb.ANN_ID}, expected format HH:MM") - return - } - - } - - "[SHALAT]" -> { - val value = variables["SHALAT"].orEmpty() - if (ValidString(value)) { - val shalat = sb.firstOrNull { it.Category == Category.Shalat.name && it.TAG == value } - if (shalat != null) { - if (ValidFile(shalat.Path)) { - files.add(shalat.Path) - } else { - cbFail.accept("Invalid soundbank file ${shalat.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") - return - } - } else { - cbFail.accept("No Shalat found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") - return - } - } else { - cbFail.accept("SHALAT variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}") - return - } - - } - - "[BCB]" -> { - // BCB bisa angka saja, misalnya 1,2,3 - // atau huruf dan angka, misalnya A1, B2, C3, 1A, 2B, 3C - val value = variables["BCB"].orEmpty() - val path = Get_Soundbank_AlpabethNumeric(sb, value) - if (path != null) { - files.addAll(path) - } else { - cbFail.accept("BCB variable is missing, empty, or doesn't have valid soundbank for value '$value' in messagebank id ${mb.ANN_ID}") - return - } - } - - "[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 { vv -> - val path = Get_Soundbank_AlpabethNumeric(sb, vv) - if (path != null) { - files.addAll(path) - } else { - cbFail.accept("GATENUMBER variable doesn't have valid soundbank for value '$vv' in messagebank id ${mb.ANN_ID}") - return - } - } - } 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 value = variables["REASON"].orEmpty() - if (ValidString(value)) { - val reason = sb.firstOrNull { it.Category == Category.Reason.name && it.TAG == value } - 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 - } - } else { - cbFail.accept("REASON variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}") - return - } - - } - - "[PROCEDURE]" -> { - val value = variables["PROCEDURE"].orEmpty() - if (ValidString(value)) { - val procedure = sb.firstOrNull { it.Category == Category.Procedure.name && it.TAG == value } - 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 { - cbFail.accept("PROCEDURE variable is missing or empty for tag $_tag in messagebank 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 - } - - } - - } - } - // all tags processed, return files - cbOK.accept(files) - - } - - jobloop@ while (isActive) { + while (isActive) { delay(1000) - // prioritas 1 , habisin queue 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(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 '${qp.Message}' to zones: ${qp.BroadcastZones}" - Logger.info { logmessage } - db.Add_Log("AAS", logmessage) - db.Delete_Queue_Paging_by_index(qp.index) - - continue@jobloop - } else { - // file tidak valid, delete from queue paging - 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 (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()) { - Get_Soundbank_Files( - mblist[0], - emptyMap(), - { - // dapat list dari files dan sudah dicek valid path - listfile -> - val listafi = mutableListOf() - listfile.forEach { filenya -> - val afi = audioPlayer.LoadAudioFile(filenya) - if (afi.isValid()) { - listafi.add(afi) - } - } - val targetfile = SoundbankResult_directory.resolve( - Make_WAV_FileName( - "Shalat", - "" - ) - ).toString() - audioPlayer.WavWriter( - listafi, - targetfile - ) { success, message -> - db.Add_Log("AAS", message) - if (success) { - // file siap broadcast - val targetafi = audioPlayer.LoadAudioFile(targetfile) - if (targetafi.isValid()) { - zz.forEach { z1 -> - StreamerOutputs.values.find { it.channel == z1 } - ?.SendData( - targetafi.bytes, - { db.Add_Log("AAS", it) }, - { db.Add_Log("AAS", it) }) - } - val logmsg = - "Broadcast started SHALAT message with generated file '$targetfile' to zones: ${qp.BroadcastZones}" - Logger.info { logmsg } - db.Add_Log("AAS", logmsg) - db.Delete_Queue_Paging_by_index(qp.index) - } else { - db.Add_Log( - "AAS", - "Failed to load generated Shalat WAV file $targetfile" - ) - } - } - } - - - }, - { err -> - db.Delete_Queue_Paging_by_index(qp.index) - db.Add_Log("AAS", err) - } - ) - } 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 { - // invalid ann_id, 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 invalid ANN_ID" - ) - } - } - } - } else { - // ada broadcast zone yang tidak valid, delete from queue paging - 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(qp.index) - db.Add_Log("AAS", "Cancelled paging message with index ${qp.index} due to empty broadcast zone") - } - } - - + subcode01.Read_Queue_Paging() // prioritas 2, habisin queue table - db.Read_Queue_Table().forEach { qa -> - if (qa.BroadcastZones.isNotEmpty()) { - val zz = qa.BroadcastZones.split(";") - if (AllBroadcastZonesValid(zz)) { - if (AllBroadcastZoneIdle(zz)) { - if (qa.Type == "SOUNDBANK") { - val variables = Get_Soundbank_Data(qa.SB_TAGS) - val languages = qa.Language.split(";") - // cek apakah ANN_ID ada di SB_TAGS - val ann_id = variables?.get("ANN_ID")?.toIntOrNull() ?: 0 - if (ann_id==0){ - // not available from variables, try to get from Message column - // ada ini, karena protokol FIS dulu tidak ada ANN_ID tapi pake Remark - val remark = variables?.get("REMARK").orEmpty() - when(remark){ - "GOP" -> { - //TODO Combobox First_Call_Message_Chooser - } - "GBD" ->{ - // TODO Combobox Second_Call_Message_Chooser - } - "GFC" ->{ - // TODO Combobox Final_Call_Message_Chooser - } - "FLD" ->{ - // TODO Combobox Landed_Message_Chooser - } - } - } - - // recheck again - if (ann_id == 0) { - db.Add_Log( - "AAS", - "Cancelled SOUNDBANK message with index ${qa.index} due to missing or invalid ANN_ID in SB_TAGS" - ) - db.Delete_Queue_Table_by_index(qa.index) - return@forEach - } - // sampe sini punya ann_id valid - - val mblist = Get_MessageBank_by_id(ann_id, languages) - if (mblist.isNotEmpty()) { - mblist.forEach { mb -> - Get_Soundbank_Files(mb, variables ?: emptyMap(), { - listfile -> - val listafi = mutableListOf() - listfile.forEach { filenya -> - val afi = audioPlayer.LoadAudioFile(filenya) - if (afi.isValid()) { - listafi.add(afi) - } - } - val targetfile = SoundbankResult_directory.resolve(Make_WAV_FileName("Soundbank","")).toString() - audioPlayer.WavWriter(listafi, targetfile - ) { success, message -> - if (success) { - // file siap broadcast - val targetafi = audioPlayer.LoadAudioFile(targetfile) - if (targetafi.isValid()) { - zz.forEach { z1 -> - StreamerOutputs.values.find { it.channel == z1 } - ?.SendData( - targetafi.bytes, - { db.Add_Log("AAS", it) }, - { db.Add_Log("AAS", it) }) - } - val logmsg = - "Broadcast started SOUNDBANK message with generated file '$targetfile' to zones: ${qa.BroadcastZones}" - Logger.info { logmsg } - db.Add_Log("AAS", logmsg) - db.Delete_Queue_Table_by_index(qa.index) - } - } - db.Add_Log("AAS", message) - } - }, - { - err -> - db.Add_Log("AAS", err) - db.Delete_Queue_Table_by_index(qa.index) - }) - } - } else { - // tidak ada messagebank dengan ann_id ini, delete from queue table - db.Delete_Queue_Table_by_index(qa.index) - db.Add_Log( - "AAS", - "Cancelled SOUNDBANK message with index ${qa.index} due to ANN_ID $ann_id not found in Messagebank" - ) - } - } else if (qa.Type == "TIMER") { - val ann_id = Get_ANN_ID(qa.SB_TAGS) - if (ann_id > 0) { - val mblist = Get_MessageBank_by_id(ann_id, qa.Language.split(";")) - if (mblist.isNotEmpty()) { - mblist.forEach { - mb -> - Get_Soundbank_Files(mb, emptyMap(), { - listfile -> - val listafi = mutableListOf() - listfile.forEach { filenya -> - val afi = audioPlayer.LoadAudioFile(filenya) - if (afi.isValid()) { - listafi.add(afi) - } - } - val targetfile = SoundbankResult_directory.resolve(Make_WAV_FileName("Timer","")).toString() - audioPlayer.WavWriter(listafi, targetfile - ) { success, message -> - if (success) { - // file siap broadcast - val targetafi = audioPlayer.LoadAudioFile(targetfile) - if (targetafi.isValid()) { - zz.forEach { z1 -> - StreamerOutputs.values.find { it.channel == z1 } - ?.SendData( - targetafi.bytes, - { db.Add_Log("AAS", it) }, - { db.Add_Log("AAS", it) }) - } - val logmsg = - "Broadcast started TIMER message with generated file '$targetfile' to zones: ${qa.BroadcastZones}" - Logger.info { logmsg } - db.Add_Log("AAS", logmsg) - db.Delete_Queue_Table_by_index(qa.index) - } - } - db.Add_Log("AAS", message) - } - }, - { - err -> - db.Add_Log("AAS", err) - db.Delete_Queue_Table_by_index(qa.index) - }) - } - - } else { - // tidak ada messagebank dengan ann_id ini, delete from queue table - db.Delete_Queue_Table_by_index(qa.index) - db.Add_Log( - "AAS", - "Cancelled TIMER with index ${qa.index} due to ANN_ID $ann_id not found in Messagebank" - ) - } - } else { - // invalid ann_id, delete from queue table - db.Delete_Queue_Table_by_index(qa.index) - db.Add_Log( - "AAS", - "Cancelled TIMER with index ${qa.index} due to invalid ANN_ID" - ) - } - } - - } - } else { - // ada broadcast zone yang tidak valid, delete from queue table - db.Delete_Queue_Table_by_index(qa.index) - db.Add_Log( - "AAS", - "Cancelled table message with index ${qa.index} due to invalid broadcast zone" - ) - } - } else { - // invalid broadcast zone, delete from queue table - db.Delete_Queue_Table_by_index(qa.index) - db.Add_Log("AAS", "Cancelled table message with index ${qa.index} due to empty broadcast zone") - } - } - + subcode01.Read_Queue_Table() } - } // Coroutine untuk cek Schedulebank tiap menit saat detik 00 CoroutineScope(Dispatchers.Default).launch { while (isActive) { delay(1000) - val localtime = LocalTime.now() - // detik harus 00 - if (localtime.second != 0) continue - val timestring = timeformat2.format(localtime) - val sch = db.SchedulebankList.filter { - it.Time == timestring && it.Enable - } - // tidak ada schedule dengan time sekarang dan enable=true - if (sch.isEmpty()) continue - - val localdate = LocalDate.now() - val ddmmyyyy = dateformat1.format(localdate) - // check special date dulu - val specialdate = sch.find { - it.Day == ddmmyyyy - } - if (specialdate != null) { - // TODO Masukin ke queue table sebagai schedule special date - - } - // cek weekly schedule - val weekly = sch.find { - it.Day == when (localdate.dayOfWeek) { - DayOfWeek.MONDAY -> ScheduleDay.Monday.name - DayOfWeek.TUESDAY -> ScheduleDay.Tuesday.name - DayOfWeek.WEDNESDAY -> ScheduleDay.Wednesday.name - DayOfWeek.THURSDAY -> ScheduleDay.Thursday.name - DayOfWeek.FRIDAY -> ScheduleDay.Friday.name - DayOfWeek.SATURDAY -> ScheduleDay.Saturday.name - DayOfWeek.SUNDAY -> ScheduleDay.Sunday.name - } - } - if (weekly != null) { - // TODO Masukin ke queue table sebagai schedule weekly - - } - // check daily schedule - val daily = sch.find { - it.Day == ScheduleDay.Everyday.name - } - if (daily != null) { - // TODO Masukin ke queue table sebagai schedule daily - - } - - + subcode01.Read_Schedule_Table() } } @@ -871,10 +70,15 @@ fun main() { listOf( Pair("admin", "password"), Pair("user", "password") - ), db, StreamerOutputs - ) + )) web.Start() + val androidserver = TCP_Android_Command_Server() + androidserver.StartTcpServer(5003){ + Logger.info { it } + db.Add_Log("ANDROID", it) + } + val barixserver = TCP_Barix_Command_Server() barixserver.StartTcpServer { cmd -> Logger.info { cmd } @@ -914,6 +118,7 @@ fun main() { Runtime.getRuntime().addShutdownHook(Thread { Logger.info { "Shutdown hook called, stopping services..." } barixserver.StopTcpCommand() + androidserver.StopTcpCommand() onlinechecker.cancel() web.Stop() audioPlayer.Close() diff --git a/src/MainExtension01.kt b/src/MainExtension01.kt new file mode 100644 index 0000000..534c2c3 --- /dev/null +++ b/src/MainExtension01.kt @@ -0,0 +1,838 @@ +import audio.AudioFileInfo +import codes.Somecodes.Companion.Get_ANN_ID +import codes.Somecodes.Companion.IsAlphabethic +import codes.Somecodes.Companion.IsNumber +import codes.Somecodes.Companion.Make_WAV_FileName +import codes.Somecodes.Companion.SoundbankResult_directory +import codes.Somecodes.Companion.ValidFile +import codes.Somecodes.Companion.ValidString +import codes.Somecodes.Companion.dateformat1 +import codes.Somecodes.Companion.timeformat2 +import content.Category +import content.Language +import content.ScheduleDay +import database.Messagebank +import database.Soundbank +import org.tinylog.Logger +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.LocalTime +import java.util.function.Consumer + +/** + * MainExtension01 contains additional functions for the main application. + * Main responsibilities are : + * 1. Reading and processing Queue_Paging + * 2. Reading and processing Queue_Table + * 3. Reading and processing Schedule + * This class is separated to keep the main.kt file cleaner and more organized. + */ +class MainExtension01 { + + /** + * 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 + } + + /** + * 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, languages: List = urutan_bahasa): ArrayList { + val mb_list = ArrayList() + 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 path for AlphabetNumeric category based on value + * @param sb List of Soundbank to search + * @param value String value to search, can be combination of letters and numbers, e.g. A1, B2, 3C, 12, etc. + * @return Soundbank path if found and valid, null if not found or invalid + */ + fun Get_Soundbank_AlpabethNumeric(sb: List, value: String): List? { + val result = mutableListOf() + if (ValidString(value)) { + if (sb.isNotEmpty()) { + val regex = Regex("([A-Z])?(\\d+)([A-Z])?") + val match = regex.find(value) + match?.groupValues?.forEach { + if (IsNumber(it)) { + val num = + sb.firstOrNull { s1 -> s1.Category == Category.AlphabetNumeric.name && s1.TAG == "N$it" } + if (num != null) { + if (ValidFile(num.Path)) { + result.add(num.Path) + } + } + } else if (IsAlphabethic(it)) { + val alp = + sb.firstOrNull { s1 -> s1.Category == Category.AlphabetNumeric.name && s1.TAG == it } + if (alp != null) { + if (ValidFile(alp.Path)) { + result.add(alp.Path) + } + } + } + } + if (result.isNotEmpty()) { + return result + } + } + } + return null + } + + val SoundbankKeywords = listOf("ANN_ID","AL","FLNUM","A_D","I_D","ORIGIN","CITY","SHALAT","PLACES","DESTINATION","ETAD","STANDCODE","GATECODE","REMARK","BCB","PLATNOMOR","REASON","PROCEDURE") + + /** + * Parse soundbank data from string value in format "KEY:VALUE KEY:VALUE ..." + * @param value String value to parse + * @return Map of key-value pairs if valid, null if invalid + */ + fun Get_Soundbank_Data(value: String) : Map? { + if (ValidString(value)){ + val values = value.split(" ").map { it.trim() }.filter { ValidString(it) } + if (values.isNotEmpty()){ + val result = mutableMapOf() + values.forEach { + val kv = it.split(":") + if (kv.size==2){ + val key = kv[0].trim().uppercase() + val val1 = kv[1].trim().uppercase() + if (ValidString(key) && ValidString(val1)){ + if (SoundbankKeywords.contains(key)) result[key] = val1 + } + } + } + if (result.isNotEmpty()) return result + } + } + return null + } + + /** + * 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 failed, returns error message + */ + fun Get_Soundbank_Files( + mb: Messagebank, + variables: Map, + cbOK: Consumer>, + cbFail: Consumer + ) { + 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() + + tags.forEach { tag -> + when (val _tag = tag.trim()) { + "[AIRPLANE_NAME]" -> { + val value = variables["AIRPLANE_NAME"].orEmpty() + if (ValidString(value)) { + val airplane = + sb.firstOrNull { it.Category == Category.Airplane_Name.name && it.TAG == value } + if (airplane != null) { + if (ValidFile(airplane.Path)) { + files.add(airplane.Path) + } else { + cbFail.accept("Invalid soundbank file ${airplane.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") + return + } + } else { + cbFail.accept("No Airplane_Name found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") + return + } + } else { + cbFail.accept("AIRPLANE_NAME variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}") + return + } + + } + + "[FLIGHT_NUMBER]" -> { + val alcode = variables["AIRPLANE_NAME"].orEmpty() + val fncode = variables["FLIGHT_NUMBER"].orEmpty() + if (ValidString(alcode) && ValidString(fncode)) { + val val1 = sb.firstOrNull { it.Category == Category.Airline_Code.name && it.TAG == alcode } + val val2 = Get_Soundbank_AlpabethNumeric(sb, fncode) + if (val1 != null) { + if (ValidFile(val1.Path)) { + files.add(val1.Path) + } else { + cbFail.accept("Invalid soundbank file ${val1.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") + return + } + } else { + cbFail.accept("No Airline_Code found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") + return + } + if (val2 != null && val2.isNotEmpty()) { + files.addAll(val2) + } else { + cbFail.accept("No valid soundbank files found for FLIGHT_NUMBER value '$fncode' for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") + return + } + + } else { + cbFail.accept("AIRPLANE_NAME or FLIGHT_NUMBER variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}") + return + } + + } + + "[PLATNOMOR]" -> { + // plat nomor bisa huruf dan angka, atau huruf angka huruf, misalnya B123CD, AB1234EF, RI1 + val value = variables["PLATNOMOR"].orEmpty() + if (ValidString(value)) { + val regex = Regex("([A-Z]+)(\\d+)([A-Z]*)") + val match = regex.find(value) + if (match != null) { + val depan = match.groups[1]?.value // huruf depan + val tengah = match.groups[2]?.value // angka + val belakang = match.groups[3]?.value // huruf belakang, bisa kosong + // ambilin per huruf + depan?.forEach { + val dep = Get_Soundbank_AlpabethNumeric(sb, it.toString()) + if (dep != null) { + files.addAll(dep) + } + } + // ambilin per angka + tengah?.forEach { + val tgh = Get_Soundbank_AlpabethNumeric(sb, it.toString()) + if (tgh != null) { + files.addAll(tgh) + } + } + // ambilin per huruf + belakang?.forEach { + val blk = Get_Soundbank_AlpabethNumeric(sb, it.toString()) + if (blk != null) { + files.addAll(blk) + } + } + } else { + cbFail.accept("PLATNOMOR variable has invalid format for value '$value' in messagebank id ${mb.ANN_ID}") + return + } + } else { + cbFail.accept("PLATNOMOR variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}") + return + } + } + + "[CITY]" -> { + val values = variables["CITY"].orEmpty().split(";").map { it.trim() }.filter { ValidString(it) } + if (values.isNotEmpty()) { + values.forEach { vv -> + val city = sb.firstOrNull { it.Category == Category.City.name && it.TAG == vv } + if (city != null) { + if (ValidFile(city.Path)) { + files.add(city.Path) + } else { + cbFail.accept("Invalid soundbank file ${city.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") + return + } + } else { + cbFail.accept("No City found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") + return + } + } + } else { + cbFail.accept("CITY variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}") + return + } + } + + "[PLACES]" -> { + val value = variables["PLACES"].orEmpty() + if (ValidString(value)) { + val places = sb.firstOrNull { it.Category == Category.Places.name && it.TAG == value } + if (places != null) { + if (ValidFile(places.Path)) { + files.add(places.Path) + } else { + cbFail.accept("Invalid soundbank file ${places.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") + return + } + } else { + cbFail.accept("No Places found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") + return + } + } else { + cbFail.accept("PLACES variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}") + return + } + + } + + "[ETAD]" -> { + val values = variables["ETAD"].orEmpty().split(":").map { it.trim() }.filter { IsNumber(it) } + if (values.size == 2) { + val hh = Get_Soundbank_AlpabethNumeric(sb, values[0]) + val mm = Get_Soundbank_AlpabethNumeric(sb, values[1]) + if (hh != null && mm != null && hh.isNotEmpty() && mm.isNotEmpty()) { + if (ValidFile(hh[0]) && ValidFile(mm[0])) { + files.add(hh[0]) + files.add(mm[0]) + } else { + cbFail.accept("ETAD variable has invalid soundbank files for tag $_tag in messagebank id ${mb.ANN_ID}, unable to find valid soundbank files for HH='${values[0]}' or MM='${values[1]}'") + return + } + } else { + cbFail.accept("ETAD variable has invalid soundbank for tag $_tag in messagebank id ${mb.ANN_ID}, unable to find soundbank for HH='${values[0]}' or MM='${values[1]}'") + return + } + } else { + cbFail.accept("ETAD variable has invalid format for tag $_tag in messagebank id ${mb.ANN_ID}, expected format HH:MM") + return + } + + } + + "[SHALAT]" -> { + val value = variables["SHALAT"].orEmpty() + if (ValidString(value)) { + val shalat = sb.firstOrNull { it.Category == Category.Shalat.name && it.TAG == value } + if (shalat != null) { + if (ValidFile(shalat.Path)) { + files.add(shalat.Path) + } else { + cbFail.accept("Invalid soundbank file ${shalat.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") + return + } + } else { + cbFail.accept("No Shalat found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}") + return + } + } else { + cbFail.accept("SHALAT variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}") + return + } + + } + + "[BCB]" -> { + // BCB bisa angka saja, misalnya 1,2,3 + // atau huruf dan angka, misalnya A1, B2, C3, 1A, 2B, 3C + val value = variables["BCB"].orEmpty() + val path = Get_Soundbank_AlpabethNumeric(sb, value) + if (path != null) { + files.addAll(path) + } else { + cbFail.accept("BCB variable is missing, empty, or doesn't have valid soundbank for value '$value' in messagebank id ${mb.ANN_ID}") + return + } + } + + "[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 { vv -> + val path = Get_Soundbank_AlpabethNumeric(sb, vv) + if (path != null) { + files.addAll(path) + } else { + cbFail.accept("GATENUMBER variable doesn't have valid soundbank for value '$vv' in messagebank id ${mb.ANN_ID}") + return + } + } + } 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 value = variables["REASON"].orEmpty() + if (ValidString(value)) { + val reason = sb.firstOrNull { it.Category == Category.Reason.name && it.TAG == value } + 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 + } + } else { + cbFail.accept("REASON variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}") + return + } + + } + + "[PROCEDURE]" -> { + val value = variables["PROCEDURE"].orEmpty() + if (ValidString(value)) { + val procedure = sb.firstOrNull { it.Category == Category.Procedure.name && it.TAG == value } + 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 { + cbFail.accept("PROCEDURE variable is missing or empty for tag $_tag in messagebank 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 + } + + } + + } + } + // all tags processed, return files + cbOK.accept(files) + + } + + /** + * Read and process Queue_Paging table. + */ + fun Read_Queue_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(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 '${qp.Message}' to zones: ${qp.BroadcastZones}" + Logger.info { logmessage } + db.Add_Log("AAS", logmessage) + db.Delete_Queue_Paging_by_index(qp.index) + + return + } else { + // file tidak valid, delete from queue paging + 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 (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()) { + Get_Soundbank_Files( + mblist[0], + emptyMap(), + { + // dapat list dari files dan sudah dicek valid path + listfile -> + val listafi = mutableListOf() + listfile.forEach { filenya -> + val afi = audioPlayer.LoadAudioFile(filenya) + if (afi.isValid()) { + listafi.add(afi) + } + } + val targetfile = SoundbankResult_directory.resolve( + Make_WAV_FileName( + "Shalat", + "" + ) + ).toString() + audioPlayer.WavWriter( + listafi, + targetfile + ) { success, message -> + db.Add_Log("AAS", message) + if (success) { + // file siap broadcast + val targetafi = audioPlayer.LoadAudioFile(targetfile) + if (targetafi.isValid()) { + zz.forEach { z1 -> + StreamerOutputs.values.find { it.channel == z1 } + ?.SendData( + targetafi.bytes, + { db.Add_Log("AAS", it) }, + { db.Add_Log("AAS", it) }) + } + val logmsg = + "Broadcast started SHALAT message with generated file '$targetfile' to zones: ${qp.BroadcastZones}" + Logger.info { logmsg } + db.Add_Log("AAS", logmsg) + db.Delete_Queue_Paging_by_index(qp.index) + } else { + db.Add_Log( + "AAS", + "Failed to load generated Shalat WAV file $targetfile" + ) + } + } + } + + + }, + { err -> + db.Delete_Queue_Paging_by_index(qp.index) + db.Add_Log("AAS", err) + } + ) + } 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 { + // invalid ann_id, 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 invalid ANN_ID" + ) + } + } + } + } else { + // ada broadcast zone yang tidak valid, delete from queue paging + 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(qp.index) + db.Add_Log("AAS", "Cancelled paging message with index ${qp.index} due to empty broadcast zone") + } + } + + } + + /** + * Read and process Queue_Table table. + */ + fun Read_Queue_Table(){ + db.Read_Queue_Table().forEach { qa -> + if (qa.BroadcastZones.isNotEmpty()) { + val zz = qa.BroadcastZones.split(";") + if (AllBroadcastZonesValid(zz)) { + if (AllBroadcastZoneIdle(zz)) { + if (qa.Type == "SOUNDBANK") { + val variables = Get_Soundbank_Data(qa.SB_TAGS) + val languages = qa.Language.split(";") + // cek apakah ANN_ID ada di SB_TAGS + val ann_id = variables?.get("ANN_ID")?.toIntOrNull() ?: 0 + if (ann_id==0){ + // not available from variables, try to get from Message column + // ada ini, karena protokol FIS dulu tidak ada ANN_ID tapi pake Remark + val remark = variables?.get("REMARK").orEmpty() + when(remark){ + "GOP" -> { + //TODO Combobox First_Call_Message_Chooser + } + "GBD" ->{ + // TODO Combobox Second_Call_Message_Chooser + } + "GFC" ->{ + // TODO Combobox Final_Call_Message_Chooser + } + "FLD" ->{ + // TODO Combobox Landed_Message_Chooser + } + } + } + + // recheck again + if (ann_id == 0) { + db.Add_Log( + "AAS", + "Cancelled SOUNDBANK message with index ${qa.index} due to missing or invalid ANN_ID in SB_TAGS" + ) + db.Delete_Queue_Table_by_index(qa.index) + return@forEach + } + // sampe sini punya ann_id valid + + val mblist = Get_MessageBank_by_id(ann_id, languages) + if (mblist.isNotEmpty()) { + mblist.forEach { mb -> + Get_Soundbank_Files(mb, variables ?: emptyMap(), { + listfile -> + val listafi = mutableListOf() + listfile.forEach { filenya -> + val afi = audioPlayer.LoadAudioFile(filenya) + if (afi.isValid()) { + listafi.add(afi) + } + } + val targetfile = SoundbankResult_directory.resolve(Make_WAV_FileName("Soundbank","")).toString() + audioPlayer.WavWriter(listafi, targetfile + ) { success, message -> + if (success) { + // file siap broadcast + val targetafi = audioPlayer.LoadAudioFile(targetfile) + if (targetafi.isValid()) { + zz.forEach { z1 -> + StreamerOutputs.values.find { it.channel == z1 } + ?.SendData( + targetafi.bytes, + { db.Add_Log("AAS", it) }, + { db.Add_Log("AAS", it) }) + } + val logmsg = + "Broadcast started SOUNDBANK message with generated file '$targetfile' to zones: ${qa.BroadcastZones}" + Logger.info { logmsg } + db.Add_Log("AAS", logmsg) + db.Delete_Queue_Table_by_index(qa.index) + + } + } + db.Add_Log("AAS", message) + } + }, + { + err -> + db.Add_Log("AAS", err) + db.Delete_Queue_Table_by_index(qa.index) + }) + } + } else { + // tidak ada messagebank dengan ann_id ini, delete from queue table + db.Delete_Queue_Table_by_index(qa.index) + db.Add_Log( + "AAS", + "Cancelled SOUNDBANK message with index ${qa.index} due to ANN_ID $ann_id not found in Messagebank" + ) + } + } else if (qa.Type == "TIMER") { + val ann_id = Get_ANN_ID(qa.SB_TAGS) + if (ann_id > 0) { + val mblist = Get_MessageBank_by_id(ann_id, qa.Language.split(";")) + if (mblist.isNotEmpty()) { + mblist.forEach { + mb -> + Get_Soundbank_Files(mb, emptyMap(), { + listfile -> + val listafi = mutableListOf() + listfile.forEach { filenya -> + val afi = audioPlayer.LoadAudioFile(filenya) + if (afi.isValid()) { + listafi.add(afi) + } + } + val targetfile = SoundbankResult_directory.resolve(Make_WAV_FileName("Timer","")).toString() + audioPlayer.WavWriter(listafi, targetfile + ) { success, message -> + if (success) { + // file siap broadcast + val targetafi = audioPlayer.LoadAudioFile(targetfile) + if (targetafi.isValid()) { + zz.forEach { z1 -> + StreamerOutputs.values.find { it.channel == z1 } + ?.SendData( + targetafi.bytes, + { db.Add_Log("AAS", it) }, + { db.Add_Log("AAS", it) }) + } + val logmsg = + "Broadcast started TIMER message with generated file '$targetfile' to zones: ${qa.BroadcastZones}" + Logger.info { logmsg } + db.Add_Log("AAS", logmsg) + db.Delete_Queue_Table_by_index(qa.index) + } + } + db.Add_Log("AAS", message) + } + }, + { + err -> + db.Add_Log("AAS", err) + db.Delete_Queue_Table_by_index(qa.index) + }) + } + + } else { + // tidak ada messagebank dengan ann_id ini, delete from queue table + db.Delete_Queue_Table_by_index(qa.index) + db.Add_Log( + "AAS", + "Cancelled TIMER with index ${qa.index} due to ANN_ID $ann_id not found in Messagebank" + ) + } + } else { + // invalid ann_id, delete from queue table + db.Delete_Queue_Table_by_index(qa.index) + db.Add_Log( + "AAS", + "Cancelled TIMER with index ${qa.index} due to invalid ANN_ID" + ) + } + } + + } + } else { + // ada broadcast zone yang tidak valid, delete from queue table + db.Delete_Queue_Table_by_index(qa.index) + db.Add_Log( + "AAS", + "Cancelled table message with index ${qa.index} due to invalid broadcast zone" + ) + } + } else { + // invalid broadcast zone, delete from queue table + db.Delete_Queue_Table_by_index(qa.index) + db.Add_Log("AAS", "Cancelled table message with index ${qa.index} due to empty broadcast zone") + } + } + } + + /** + * Read and process Schedule_Table table. + * This function is called every minute when second=00. + * It checks for schedules that match the current time and day, + * and adds them to the Queue_Table for processing. + */ + fun Read_Schedule_Table(){ + val localtime = LocalTime.now() + // detik harus 00 + if (localtime.second != 0) return + val timestring = timeformat2.format(localtime) + val sch = db.SchedulebankList.filter { + it.Time == timestring && it.Enable + } + // tidak ada schedule dengan time sekarang dan enable=true + if (sch.isEmpty()) return + + val localdate = LocalDate.now() + val ddmmyyyy = dateformat1.format(localdate) + // check special date dulu + val specialdate = sch.find { + it.Day == ddmmyyyy + } + if (specialdate != null) { + // TODO Masukin ke queue table sebagai schedule special date + + } + // cek weekly schedule + val weekly = sch.find { + it.Day == when (localdate.dayOfWeek) { + DayOfWeek.MONDAY -> ScheduleDay.Monday.name + DayOfWeek.TUESDAY -> ScheduleDay.Tuesday.name + DayOfWeek.WEDNESDAY -> ScheduleDay.Wednesday.name + DayOfWeek.THURSDAY -> ScheduleDay.Thursday.name + DayOfWeek.FRIDAY -> ScheduleDay.Friday.name + DayOfWeek.SATURDAY -> ScheduleDay.Saturday.name + DayOfWeek.SUNDAY -> ScheduleDay.Sunday.name + } + } + if (weekly != null) { + // TODO Masukin ke queue table sebagai schedule weekly + + } + // check daily schedule + val daily = sch.find { + it.Day == ScheduleDay.Everyday.name + } + if (daily != null) { + // TODO Masukin ke queue table sebagai schedule daily + + } + } +} \ No newline at end of file diff --git a/src/commandServer/TCP_Android_Command_Server.kt b/src/commandServer/TCP_Android_Command_Server.kt index 16989c2..04f5aa5 100644 --- a/src/commandServer/TCP_Android_Command_Server.kt +++ b/src/commandServer/TCP_Android_Command_Server.kt @@ -10,6 +10,8 @@ import kotlinx.coroutines.runBlocking import org.tinylog.Logger import java.net.ServerSocket import java.net.Socket +import java.nio.ByteBuffer +import java.nio.ByteOrder import java.util.function.Consumer @Suppress("unused") @@ -17,14 +19,16 @@ class TCP_Android_Command_Server { private var tcpserver: ServerSocket? = null private var job: Job? = null private val socketMap = mutableMapOf() + lateinit var logcb: Consumer /** * Start TCP Command Server * @param port The port to listen on, default is 5003 - * @param cb The callback function to handle incoming messages + * @param logCB Callback to handle Log messages * @return true if successful */ - fun StartTcpServer(port: Int = 5003, cb: Consumer): Boolean { + fun StartTcpServer(port: Int = 5003, logCB: Consumer): Boolean { + logcb = logCB try { val tcp = ServerSocket(port) tcpserver = tcp @@ -37,27 +41,32 @@ class TCP_Android_Command_Server { { CoroutineScope(Dispatchers.Main).launch { if (socket != null) { - val key : String = socket.inetAddress.hostAddress+":"+socket.port + val key: String = socket.inetAddress.hostAddress + ":" + socket.port socketMap[key] = socket Logger.info { "Start communicating with $key" } - socket.getInputStream().use { din -> + socket.getInputStream().let { din -> { while (isActive) { - if (din.available()>0){ + if (din.available() > 0) { val bb = ByteArray(din.available()) din.read(bb) // B4A format, 4 bytes di depan adalah size - val str = String(bb,4,bb.size-4) - str.split("@").forEach { - if (ValidString(it)){ - cb.accept(it) + val str = String(bb, 4, bb.size - 4) + str.split("@").map { it.trim() }.filter { ValidString(it) } + .map { it.uppercase() }.forEach { + process_command(it) { reply -> + try { + socket.getOutputStream().write(String_to_Byte_Android(reply)) + } catch (e: Exception) { + logcb.accept("Failed to send reply to $key, Message : ${e.message}") + } } } } } } } - Logger.info { "Finished communicating with $key" } + logcb.accept("Finished communicatiing with $key") socketMap.remove(key) } @@ -66,19 +75,84 @@ class TCP_Android_Command_Server { } } catch (ex: Exception) { - Logger.error { "Failed accepting TCP Socket, Message : ${ex.message}" } + logcb.accept("Failed accepting TCP Socket, Message : ${ex.message}") } } - Logger.info { "TCP server stopped" } + logcb.accept("TCP server stopped") } return true } catch (e: Exception) { - Logger.error { "Failed to StartTcpServer, Message : ${e.message}" } + logcb.accept("Failed to StartTcpServer, Message : ${e.message}") } return false } + /** + * Convert a String to ByteArray in prefix AsyncStream format in B4X + * @param str The input string + * @return ByteArray with 4 bytes prefix length + string bytes + */ + private fun String_to_Byte_Android(str: String): ByteArray { + if (ValidString(str)) { + val len = str.length + val bytes = str.toByteArray() + return ByteBuffer.allocate(len + 4) + .order(ByteOrder.LITTLE_ENDIAN) + .putInt(len) + .put(bytes) + .array() + } + return ByteArray(0) + } + + private fun process_command(cmd: String, cb: Consumer) { + Logger.info { "Command from Android: $cmd" } + val parts = cmd.split(";").map { it.trim() }.filter { it.isNotBlank() }.map { it.uppercase() } + when (parts[0]) { + "GETLOGIN" -> { + val username = parts.getOrElse(1) { "" } + val password = parts.getOrElse(2) { "" } + if (ValidString(username) && ValidString(password)) { + //TODO handle login here + } else cb.accept("LOGIN;FALSE@") + } + + "PCMFILE_START" -> { + // TODO read coding here + } + + "PCMFILE_STOP" -> { + // TODO read coding here + } + + "STARTPAGINGAND" -> { + // TODO read coding here + } + + "STOPPAGINGAND" -> { + // TODO read coding here + } + + "CANCELPAGINGAND" -> { + // TODO read coding here + } + + "STARTINITIALIZE" -> { + // TODO read coding here + } + + "BROADCASTAND" -> { + // TODO read coding here + } + + else -> { + logcb.accept("Unknown command from Android: $cmd") + } + + } + } + /** * Stop TCP Command Server * @return true if succesful diff --git a/src/database/MariaDB.kt b/src/database/MariaDB.kt index bf8eea0..18c8905 100644 --- a/src/database/MariaDB.kt +++ b/src/database/MariaDB.kt @@ -2218,4 +2218,25 @@ class MariaDB( return false } + + fun Get_User_List(): ArrayList { + val userList = ArrayList() + try { + val statement = connection?.createStatement() + val resultSet = statement?.executeQuery("SELECT * FROM user") + while (resultSet?.next() == true) { + val user = UserDB( + resultSet.getLong("index").toUInt(), + resultSet.getString("username"), + resultSet.getString("password"), + resultSet.getString("location") + ) + userList.add(user) + } + } catch (e: Exception) { + Logger.error("Error fetching user list: ${e.message}" as Any) + } + return userList + } + } \ No newline at end of file diff --git a/src/database/dbFunctions.kt b/src/database/dbFunctions.kt new file mode 100644 index 0000000..622abeb --- /dev/null +++ b/src/database/dbFunctions.kt @@ -0,0 +1,20 @@ +package database + +import java.sql.Connection + +abstract class dbFunctions(val dbName: String, val connection: Connection) { + var List : ArrayList = ArrayList() + fun Clear(){ + + } + + fun DeleteByIndex(index: Int) { + + } + + abstract fun Create() + abstract fun Get(): ArrayList + abstract fun Add(data: T): Boolean + abstract fun UpdateByIndex(index: Int, data: T): Boolean + abstract fun Resort(): Boolean +} \ No newline at end of file diff --git a/src/web/WebApp.kt b/src/web/WebApp.kt index cad3c8c..9f6009f 100644 --- a/src/web/WebApp.kt +++ b/src/web/WebApp.kt @@ -1,6 +1,6 @@ package web -import barix.BarixConnection +import StreamerOutputs import codes.Somecodes import codes.Somecodes.Companion.ListAudioFiles import codes.Somecodes.Companion.ValiDateForLogHtml @@ -21,6 +21,7 @@ import database.MariaDB import database.Messagebank import database.SoundChannel import database.Soundbank +import db import io.javalin.Javalin import io.javalin.apibuilder.ApiBuilder.before import io.javalin.apibuilder.ApiBuilder.delete @@ -36,7 +37,7 @@ import org.apache.poi.xssf.usermodel.XSSFWorkbook import java.time.LocalDateTime @Suppress("unused") -class WebApp(val listenPort: Int, val userlist: List>, val db: MariaDB, val StreamerOutputs : MutableMap ) { +class WebApp(val listenPort: Int, val userlist: List>) { var app: Javalin? = null val objectmapper = jacksonObjectMapper()