package commandServer import audioPlayer import codes.Somecodes.Companion.ValidString import codes.Somecodes.Companion.datetimeformat1 import content.Language import database.Messagebank import database.QueuePaging import database.QueueTable import database.Soundbank import db import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.tinylog.Logger import udpreceiver import java.net.ServerSocket import java.net.Socket import java.nio.ByteBuffer import java.nio.ByteOrder import java.time.LocalDateTime import java.util.function.Consumer import kotlin.io.path.absolutePathString @Suppress("unused") class TCP_Android_Command_Server { lateinit var tcpserver: ServerSocket lateinit var job: Job private val socketMap = mutableMapOf() lateinit var logcb: Consumer private val listUserLogin = mutableListOf() private val listOnGoingPaging = mutableMapOf() /** * Start TCP Command Server * @param port The port to listen on, default is 5003 * @param logCB Callback to handle Log messages * @return true if successful */ fun StartTcpServer(port: Int = 5003, logCB: Consumer): Boolean { logcb = logCB try { val tcp = ServerSocket(port) tcpserver = tcp job = CoroutineScope(Dispatchers.IO).launch { Logger.info { "TCP Android server started on port $port" } while (isActive) { if (tcpserver.isClosed) break try { tcpserver.accept().use { socket -> { CoroutineScope(Dispatchers.IO).launch { if (socket != null) { // key is IP address only val key: String = socket.inetAddress.hostAddress socketMap[key] = socket Logger.info { "Start communicating with $key" } socket.getInputStream().let { din -> { while (isActive) { if (din.available() > 0) { val bb = ByteArray(din.available()) din.read(bb) // B4A format, 4 bytes di depan adalah size val str = String(bb, 4, bb.size - 4) str.split("@").map { it.trim() }.filter { ValidString(it) } .map { it.uppercase() }.forEach { process_command(key,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}") } } } } } } } logcb.accept("Finished communicatiing with $key") socketMap.remove(key) } } } } } catch (ex: Exception) { logcb.accept("Failed accepting TCP Socket, Message : ${ex.message}") } } logcb.accept("TCP server stopped") } return true } catch (e: Exception) { 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) } /** * Process command from Android client * @param key The client IP address * @param cmd The command string * @param cb Callback to send reply string */ private fun process_command(key: String, cmd: String, cb: Consumer) { Logger.info { "Command from $key : $cmd" } val parts = cmd.split(";").map { it.trim() }.filter { it.isNotBlank() }.map { it.uppercase() } when (parts[0]) { "GETLOGIN" -> { // Android login request val username = parts.getOrElse(1) { "" } val password = parts.getOrElse(2) { "" } if (ValidString(username) && ValidString(password)) { if (db.userDB.List.any{it.username==username && it.password==password}) { val existing = listUserLogin.find { it.ip == key} if (existing!=null){ existing.username = username } else{ listUserLogin.add(userLogin(key, username)) } cb.accept("LOGIN;TRUE@") logcb.accept("Android Login success from $key as $username") return } else { logcb.accept("Android Login failed from $key as $username") cb.accept("LOGIN;FALSE@") } } else { logcb.accept("Android Login failed from $key with empty username or password") cb.accept("LOGIN;FALSE@") } } "PCMFILE_START","STARTPAGINGAND" -> { val zones = parts.getOrElse(3) { "" }.replace(",",";") if (ValidString(zones)){ // create pagingjob val pj = PagingJob(key, zones) // masukin ke list listOnGoingPaging[key] = pj // start minta data dari udpreceiver udpreceiver.RequestDataFrom(key){ // push data ke paging job pj.addData(it, it.size) } logcb.accept("Paging started from Android $key") cb.accept(parts[0]+";OK@") return } else logcb.accept("Paging start from Android $key failed, empty zones") cb.accept(parts[0]+";NG@") } "PCMFILE_STOP","STOPPAGINGAND" -> { val pj = listOnGoingPaging[key] if (pj!=null){ listOnGoingPaging.remove(key) udpreceiver.StopRequestDataFrom(key) logcb.accept("Paging stopped from Android $key") cb.accept(parts[0]+";OK@") // get remaining data val data = pj.GetData() pj.Close() audioPlayer.WavWriter(data, pj.filePath.absolutePathString()){ success, message -> if (success){ // insert to paging queue val qp = QueuePaging( 0u, LocalDateTime.now().format(datetimeformat1), "PAGING", "NORMAL", pj.filePath.absolutePathString(), pj.broadcastzones ) if (db.queuepagingDB.Add(qp)){ logcb.accept("Paging audio inserted to queue paging table from Android $key, file ${pj.filePath.absolutePathString()}") cb.accept(parts[0]+";OK@") } else { logcb.accept("Failed to insert paging audio to queue paging table from Android $key, file ${pj.filePath.absolutePathString()}") cb.accept(parts[0]+";NG@") } } else { logcb.accept("Failed to write paging audio to file ${pj.filePath.absolutePathString()}, Message : $message") cb.accept(parts[0]+";NG@") } } } else { logcb.accept("Paging stop from Android $key failed, no ongoing paging") cb.accept(parts[0]+";NG@") } } "CANCELPAGINGAND" -> { val pj = listOnGoingPaging[key] if (pj!=null){ pj.Close() listOnGoingPaging.remove(key) udpreceiver.StopRequestDataFrom(key) logcb.accept("Paging from Android $key cancelled") cb.accept("CANCELPAGINGAND;OK@") return } else logcb.accept("Paging cancel from Android $key failed, no ongoing paging") cb.accept("CANCELPAGINGAND;NG@") } "STARTINITIALIZE" -> { // pengiriman variabel ke Android val username = parts.getOrElse(1) { "" } if (ValidString(username)){ val userlogin = listUserLogin.find { it.username == username } if (userlogin != null){ val userdb = db.userDB.List.find { it.username == username } if (userdb != null){ val result = StringBuilder() result.append("ZONE") userdb.broadcastzones.split(";").map { it.trim() }.filter { it.isNotBlank() }.forEach { result.append(";") result.append(it) } result.append("@") val VARMESSAGES = mutableListOf() userdb.messagebank_ann_id // messagebank_ann_id adalah rentengan ANN_ID (digit) yang dipisah dengan ; .split(";") // trim dulu .map { it.trim() } // bukan string kosong antar dua tanda ; .filter { it.isNotBlank() } // beneran digit semua .filter { xx -> xx.all{it.isDigit()} } // iterasi setiap ANN_ID .forEach { annid -> // masukin ke VARMESSAGES yang unik secara ANN_ID dan Language val xx = db.messageDB.List .filter{ it.ANN_ID == annid.toUInt() } .distinctBy { it.Language } VARMESSAGES.addAll(xx) } result.append("MSGTOTAL;").append(VARMESSAGES.size).append("@") // VAR AP TOTAL val VARAPTOTAL = mutableListOf() val sb_split = userdb.soundbank_tags.split(";").map { it.trim() }.filter { it.isNotBlank() } sb_split.forEach { val sb = db.Find_Soundbank_AirplaneName(it).firstOrNull() if (sb != null) VARAPTOTAL.add(sb) } result.append("VARAPTOTAL;").append(VARAPTOTAL.size).append("@") // VAR CITY TOTAL val VARCITYTOTAL = mutableListOf() sb_split.forEach { val sb = db.Find_Soundbank_City(it).firstOrNull() if (sb != null) VARCITYTOTAL.add(sb) } result.append("VARCITYTOTAL;").append(VARCITYTOTAL.size).append("@") // VAR PLACES TOTAL val VARPLACESTOTAL = mutableListOf() sb_split.forEach { val sb = db.Find_Soundbank_Places(it).firstOrNull() if (sb != null) VARPLACESTOTAL.add(sb) } result.append("VARPLACESTOTAL;").append(VARPLACESTOTAL.size).append("@") // VAR SHALAT TOTAL val VARSHALATTOTAL = mutableListOf() sb_split.forEach { val sb = db.Find_Soundbank_Shalat(it).firstOrNull() if (sb != null) VARSHALATTOTAL.add(sb) } result.append("VARSHALATTOTAL;").append(VARSHALATTOTAL.size).append("@") // VAR SEQUENCE TOTAL val VARSEQUENCETOTAL = mutableListOf() sb_split.forEach { val sb = db.Find_Soundbank_Sequence(it).firstOrNull() if (sb != null) VARSEQUENCETOTAL.add(sb) } // VAR REASON TOTAL val VARREASONTOTAL = mutableListOf() sb_split.forEach { val sb = db.Find_Soundbank_Reason(it).firstOrNull() if (sb != null) VARREASONTOTAL.add(sb) } result.append("VARREASONTOTAL;").append(VARREASONTOTAL.size).append("@") // VAR PROCEDURE TOTAL val VARPROCEDURETOTAL = mutableListOf() sb_split.forEach { val sb = db.Find_Soundbank_Procedure(it).firstOrNull() if (sb != null) VARPROCEDURETOTAL.add(sb) } result.append("VARPROCEDURETOTAL;").append(VARPROCEDURETOTAL.size).append("@") // send to sender cb.accept(result.toString()) result.clear() //Append MSG, for Android only Indonesia and English VARMESSAGES.groupBy { it.ANN_ID }.forEach { (ann_id, value) -> result.append("MSG;").append(ann_id) result.append(";") value.find { it.Language.equals(Language.INDONESIA.name, true) }?.let {result.append(it.Message_Detail)} ?: result.append("NA") result.append(";") value.find {it.Language.equals(Language.ENGLISH.name, true) }?.let {result.append(it.Message_Detail)} ?: result.append("NA") result.append("@") } // append VARAP VARAPTOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank -> result.append("VARAP;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@") } // append VARCITY VARCITYTOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank -> result.append("VARCITY;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@") } // append VARPLACES VARPLACESTOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank -> result.append("VARPLACES;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@") } // append VARSHALAT VARSHALATTOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank -> result.append("VARSHALAT;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@") } // append VARSEQUENCE VARSEQUENCETOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank -> result.append("VARSEQUENCE;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@") } // append VARREASON VARREASONTOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank -> result.append("VARREASON;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@") } // append VARPROCEDURE VARPROCEDURETOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank -> result.append("VARPROCEDURE;").append(index).append(";").append(soundbank.TAG).append(";").append(soundbank.Description).append("@") } // send to sender cb.accept(result.toString()) logcb.accept("All variables sent to $key with username $username") return } else logcb.accept("STARTINITIALIZE failed from $key with username $username not found in userDB") } else logcb.accept("STARTINITIALIZE failed from $key with unregistered username $username") } else logcb.accept("STARTINITIALIZE failed from $key with empty username") cb.accept("STARTINITIALIZE;FALSE@") } "BROADCASTAND" -> { // semi auto dari android, masukin ke queue table val desc = parts.getOrElse(1) { "" } val lang = parts.getOrElse(2) { "" }.replace(",",";") val tags = parts.getOrElse(3) { "" }.replace(",",";") val zone = parts.getOrElse(4) { "" }.replace(",",";") if (ValidString(desc)){ if (ValidString(lang)){ if (ValidString(tags)){ if (ValidString(zone)){ val qt = QueueTable( 0u, LocalDateTime.now().format(datetimeformat1), "ANDROID", "SOUNDBANK", desc, tags, zone, 1u, lang ) if (db.queuetableDB.Add(qt)){ logcb.accept("Broadcast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} inserted. Message: $desc;$lang;$tags;$zone") cb.accept("BROADCASTAND;OK@") return } else logcb.accept("Broadcast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} failed, cannot add to queue table") } else logcb.accept("Broadcast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} failed, empty zone") } else logcb.accept("Broadcsast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} failed, empty tags") } else logcb.accept("Broadcast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} failed, empty language") } else logcb.accept("Broadcast request from Android $key username=${listUserLogin.find { it.ip==key }?.username ?: "UNKNOWN"} failed, empty description") cb.accept("NG@") } else -> { logcb.accept("Unknown command from Android: $cmd") } } } /** * Stop TCP Command Server * @return true if succesful */ fun StopTcpCommand(): Boolean { try { tcpserver.close() runBlocking { socketMap.values.forEach { it.close() } socketMap.clear() job.join() } Logger.info { "StopTcpCommand success" } return true } catch (e: Exception) { Logger.error { "Failed to StopTcpServer, Message : ${e.message}" } } return false } }