Commit 29/09/2025

This commit is contained in:
rdkartono
2025-09-29 11:56:08 +07:00
parent f18a0ca9cd
commit cf24c06b35
6 changed files with 1004 additions and 845 deletions

View File

@@ -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,64 +14,15 @@ 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
fun main() {
val version = "0.0.1 (23/09/2025)"
lateinit var db: MariaDB
lateinit var audioPlayer: AudioPlayer
val StreamerOutputs: MutableMap<String, BarixConnection> = HashMap()
const val version = "0.0.1 (23/09/2025)"
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.InitAudio(1)
val db = MariaDB()
// 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<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 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(
@@ -96,773 +34,34 @@ fun main() {
Language.ARABIC.name
)
// dipakai untuk pilih voice type, bisa diganti via web nanti
var selected_voice = VoiceType.VOICE_1.name
// Application start here
fun main() {
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" }
audioPlayer = AudioPlayer(44100) // 44100 Hz sampling rate
audioPlayer.InitAudio(1)
db = MariaDB()
/**
* 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<String> = urutan_bahasa): ArrayList<Messagebank> {
val mb_list = ArrayList<Messagebank>()
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
}
val subcode01 = MainExtension01()
/**
* 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<Soundbank>, value: String): List<String>? {
val result = mutableListOf<String>()
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<String, String>? {
if (ValidString(value)){
val values = value.split(" ").map { it.trim() }.filter { ValidString(it) }
if (values.isNotEmpty()){
val result = mutableMapOf<String, String>()
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<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 ->
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) {
// Coroutine untuk cek Paging Queue dan AAS Queue setiap detik
CoroutineScope(Dispatchers.Default).launch {
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<AudioFileInfo>()
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
subcode01.Read_Queue_Table()
}
"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<AudioFileInfo>()
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<AudioFileInfo>()
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")
}
}
}
}
// 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()

838
src/MainExtension01.kt Normal file
View File

@@ -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<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
}
/**
* 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<String> = urutan_bahasa): ArrayList<Messagebank> {
val mb_list = ArrayList<Messagebank>()
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<Soundbank>, value: String): List<String>? {
val result = mutableListOf<String>()
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<String, String>? {
if (ValidString(value)){
val values = value.split(" ").map { it.trim() }.filter { ValidString(it) }
if (values.isNotEmpty()){
val result = mutableMapOf<String, String>()
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<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 ->
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<AudioFileInfo>()
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<AudioFileInfo>()
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<AudioFileInfo>()
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
}
}
}

View File

@@ -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<String, Socket>()
lateinit var logcb: Consumer<String>
/**
* 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<String>): Boolean {
fun StartTcpServer(port: Int = 5003, logCB: Consumer<String>): Boolean {
logcb = logCB
try {
val tcp = ServerSocket(port)
tcpserver = tcp
@@ -40,7 +44,7 @@ class TCP_Android_Command_Server {
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) {
@@ -48,16 +52,21 @@ class TCP_Android_Command_Server {
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)
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<String>) {
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

View File

@@ -2218,4 +2218,25 @@ class MariaDB(
return false
}
fun Get_User_List(): ArrayList<UserDB> {
val userList = ArrayList<UserDB>()
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
}
}

View File

@@ -0,0 +1,20 @@
package database
import java.sql.Connection
abstract class dbFunctions<T>(val dbName: String, val connection: Connection) {
var List : ArrayList<T> = ArrayList()
fun Clear(){
}
fun DeleteByIndex(index: Int) {
}
abstract fun Create()
abstract fun Get(): ArrayList<T>
abstract fun Add(data: T): Boolean
abstract fun UpdateByIndex(index: Int, data: T): Boolean
abstract fun Resort(): Boolean
}

View File

@@ -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<Pair<String, String>>, val db: MariaDB, val StreamerOutputs : MutableMap<String, BarixConnection> ) {
class WebApp(val listenPort: Int, val userlist: List<Pair<String, String>>) {
var app: Javalin? = null
val objectmapper = jacksonObjectMapper()