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 audio.AudioPlayer
import barix.BarixConnection import barix.BarixConnection
import barix.TCP_Barix_Command_Server import barix.TCP_Barix_Command_Server
import codes.Somecodes.Companion.Get_ANN_ID
import codes.Somecodes.Companion.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 com.sun.jna.Platform
import content.Category import commandServer.TCP_Android_Command_Server
import content.Language import content.Language
import content.ScheduleDay
import content.VoiceType import content.VoiceType
import database.MariaDB import database.MariaDB
import database.Messagebank
import database.Soundbank
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -27,842 +14,54 @@ import kotlinx.coroutines.launch
import org.tinylog.Logger import org.tinylog.Logger
import oshi.util.GlobalConfig import oshi.util.GlobalConfig
import web.WebApp import web.WebApp
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.LocalTime
import java.util.function.Consumer
import kotlin.concurrent.fixedRateTimer import kotlin.concurrent.fixedRateTimer
lateinit var db: MariaDB
lateinit var audioPlayer: AudioPlayer
val StreamerOutputs: MutableMap<String, BarixConnection> = 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() { fun main() {
val version = "0.0.1 (23/09/2025)"
val StreamerOutputs: MutableMap<String, BarixConnection> = HashMap()
if (Platform.isWindows()) { if (Platform.isWindows()) {
// supaya OSHI bisa mendapatkan CPU usage di Windows seperti di Task Manager // supaya OSHI bisa mendapatkan CPU usage di Windows seperti di Task Manager
GlobalConfig.set(GlobalConfig.OSHI_OS_WINDOWS_CPU_UTILITY, true) GlobalConfig.set(GlobalConfig.OSHI_OS_WINDOWS_CPU_UTILITY, true)
} }
Logger.info { "Starting AAS New Generation version $version" } Logger.info { "Starting AAS New Generation version $version" }
val audioPlayer = AudioPlayer(44100) // 44100 Hz sampling rate audioPlayer = AudioPlayer(44100) // 44100 Hz sampling rate
audioPlayer.InitAudio(1) audioPlayer.InitAudio(1)
val db = MariaDB() db = MariaDB()
val subcode01 = MainExtension01()
// Coroutine untuk cek Paging Queue dan AAS Queue setiap detik // Coroutine untuk cek Paging Queue dan AAS Queue setiap detik
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
/** while (isActive) {
* Fungsi untuk cek apakah semua broadcast zone valid
* @param bz List of broadcast zone (SoundChannel)
* @return true jika semua valid, false jika ada yang tidak valid
*/
fun AllBroadcastZonesValid(bz: List<String>): Boolean {
if (bz.isNotEmpty()) {
val validchannels = bz
// check apakah tiap zone ada di database broadcast zones
.filter { z1 ->
db.BroadcastZoneList.find { z2 -> z2.SoundChannel == z1 } != null
}
// check apakah tiap zone ada di SoundChannelList dan Online
.filter { z3 ->
StreamerOutputs.any { sc -> sc.value.channel == z3 && sc.value.isOnline() }
}
// kalau jumlah valid channel sama dengan jumlah broadcast zone, berarti semua valid
return validchannels.size == bz.size
}
return false
}
/**
* Fungsi untuk cek apakah semua broadcast zone idle
* @param bz List of broadcast zone (SoundChannel)
* @return true jika semua idle, false jika ada yang tidak idle
*/
fun AllBroadcastZoneIdle(bz: List<String>): Boolean {
if (bz.isNotEmpty()) {
return bz.all { z1 ->
StreamerOutputs.any { sc -> sc.value.channel == z1 && sc.value.isIdle() }
}
}
return false
}
// dipakai untuk ambil messagebank berdasarkan id
val urutan_bahasa = listOf(
Language.INDONESIA.name,
Language.LOCAL.name,
Language.ENGLISH.name,
Language.CHINESE.name,
Language.JAPANESE.name,
Language.ARABIC.name
)
// dipakai untuk pilih voice type, bisa diganti via web nanti
var selected_voice = VoiceType.VOICE_1.name
/**
* Fungsi untuk ambil messagebank berdasarkan ANN_ID, diurutkan berdasarkan urutan bahasa di urutan_bahasa
* @param id ANN_ID dari messagebank
* @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)
}
jobloop@ while (isActive) {
delay(1000) delay(1000)
// prioritas 1 , habisin queue paging // prioritas 1 , habisin queue paging
for (qp in db.Read_Queue_Paging()) { subcode01.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")
}
}
// prioritas 2, habisin queue table // prioritas 2, habisin queue table
db.Read_Queue_Table().forEach { qa -> subcode01.Read_Queue_Table()
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")
}
}
} }
} }
// Coroutine untuk cek Schedulebank tiap menit saat detik 00 // Coroutine untuk cek Schedulebank tiap menit saat detik 00
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
while (isActive) { while (isActive) {
delay(1000) delay(1000)
val localtime = LocalTime.now() subcode01.Read_Schedule_Table()
// 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
}
} }
} }
@@ -871,10 +70,15 @@ fun main() {
listOf( listOf(
Pair("admin", "password"), Pair("admin", "password"),
Pair("user", "password") Pair("user", "password")
), db, StreamerOutputs ))
)
web.Start() 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() val barixserver = TCP_Barix_Command_Server()
barixserver.StartTcpServer { cmd -> barixserver.StartTcpServer { cmd ->
Logger.info { cmd } Logger.info { cmd }
@@ -914,6 +118,7 @@ fun main() {
Runtime.getRuntime().addShutdownHook(Thread { Runtime.getRuntime().addShutdownHook(Thread {
Logger.info { "Shutdown hook called, stopping services..." } Logger.info { "Shutdown hook called, stopping services..." }
barixserver.StopTcpCommand() barixserver.StopTcpCommand()
androidserver.StopTcpCommand()
onlinechecker.cancel() onlinechecker.cancel()
web.Stop() web.Stop()
audioPlayer.Close() 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 org.tinylog.Logger
import java.net.ServerSocket import java.net.ServerSocket
import java.net.Socket import java.net.Socket
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.function.Consumer import java.util.function.Consumer
@Suppress("unused") @Suppress("unused")
@@ -17,14 +19,16 @@ class TCP_Android_Command_Server {
private var tcpserver: ServerSocket? = null private var tcpserver: ServerSocket? = null
private var job: Job? = null private var job: Job? = null
private val socketMap = mutableMapOf<String, Socket>() private val socketMap = mutableMapOf<String, Socket>()
lateinit var logcb: Consumer<String>
/** /**
* Start TCP Command Server * Start TCP Command Server
* @param port The port to listen on, default is 5003 * @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 * @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 { try {
val tcp = ServerSocket(port) val tcp = ServerSocket(port)
tcpserver = tcp tcpserver = tcp
@@ -37,27 +41,32 @@ class TCP_Android_Command_Server {
{ {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
if (socket != null) { if (socket != null) {
val key : String = socket.inetAddress.hostAddress+":"+socket.port val key: String = socket.inetAddress.hostAddress + ":" + socket.port
socketMap[key] = socket socketMap[key] = socket
Logger.info { "Start communicating with $key" } Logger.info { "Start communicating with $key" }
socket.getInputStream().use { din -> socket.getInputStream().let { din ->
{ {
while (isActive) { while (isActive) {
if (din.available()>0){ if (din.available() > 0) {
val bb = ByteArray(din.available()) val bb = ByteArray(din.available())
din.read(bb) din.read(bb)
// B4A format, 4 bytes di depan adalah size // B4A format, 4 bytes di depan adalah size
val str = String(bb,4,bb.size-4) val str = String(bb, 4, bb.size - 4)
str.split("@").forEach { str.split("@").map { it.trim() }.filter { ValidString(it) }
if (ValidString(it)){ .map { it.uppercase() }.forEach {
cb.accept(it) 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) socketMap.remove(key)
} }
@@ -66,19 +75,84 @@ class TCP_Android_Command_Server {
} }
} catch (ex: Exception) { } 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 return true
} catch (e: Exception) { } catch (e: Exception) {
Logger.error { "Failed to StartTcpServer, Message : ${e.message}" } logcb.accept("Failed to StartTcpServer, Message : ${e.message}")
} }
return false 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 * Stop TCP Command Server
* @return true if succesful * @return true if succesful

View File

@@ -2218,4 +2218,25 @@ class MariaDB(
return false 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 package web
import barix.BarixConnection import StreamerOutputs
import codes.Somecodes import codes.Somecodes
import codes.Somecodes.Companion.ListAudioFiles import codes.Somecodes.Companion.ListAudioFiles
import codes.Somecodes.Companion.ValiDateForLogHtml import codes.Somecodes.Companion.ValiDateForLogHtml
@@ -21,6 +21,7 @@ import database.MariaDB
import database.Messagebank import database.Messagebank
import database.SoundChannel import database.SoundChannel
import database.Soundbank import database.Soundbank
import db
import io.javalin.Javalin import io.javalin.Javalin
import io.javalin.apibuilder.ApiBuilder.before import io.javalin.apibuilder.ApiBuilder.before
import io.javalin.apibuilder.ApiBuilder.delete import io.javalin.apibuilder.ApiBuilder.delete
@@ -36,7 +37,7 @@ import org.apache.poi.xssf.usermodel.XSSFWorkbook
import java.time.LocalDateTime import java.time.LocalDateTime
@Suppress("unused") @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 var app: Javalin? = null
val objectmapper = jacksonObjectMapper() val objectmapper = jacksonObjectMapper()