package commandServer import audioPlayer import codes.Somecodes.Companion.ValidString import codes.Somecodes.Companion.datetimeformat1 import content.Category 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 tcpreceiver 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 { val socket = tcpserver.accept() 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 IPMT/IPM with IP $key" } val din = socket.getInputStream() val dout = socket.getOutputStream() try{ 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) println("Received command from $key : $str") str.split("@").map { it.trim() }.filter { ValidString(it) } .forEach { process_command(key,it) { reply -> try { dout.write(String_to_Byte_Android(reply)) } catch (e: Exception) { logcb.accept("Failed to send reply to $key, Message : $e") } } } } } } catch (e : Exception){ logcb.accept("Exception in communication with $key, Message : ${e.message}") } logcb.accept("Finished communicatiing with $key") CloseSocket(socket) 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 } private fun CloseSocket(socket : Socket) { try { socket.close() } catch (e: Exception) { Logger.error { "Failed to close socket, Message : ${e.message}" } } } /** * 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 bytes = str.toByteArray(Charsets.UTF_8) val len = bytes.size 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() } 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" ->{ // start sending PCM data from Android for paging val size = parts.getOrElse(1) { "0" }.toInt() val filename = parts.getOrElse(2) { "" } val zones = parts.getOrElse(3) { "" }.replace(",",";") if (size>0){ if (ValidString(filename)){ if (ValidString(zones)){ // create paging job val pj = PagingJob(key, zones) // ada expected size pj.expectedSize = size // masukin ke list listOnGoingPaging[key] = pj Logger.info{"PagingJob created for Android $key, zones: $zones, file: ${pj.filePath.absolutePathString()}"} tcpreceiver.RequestDataFrom(key) { // push data ke paging job pj.addData(it, it.size) } cb.accept("PCMFILE_START;OK@") Logger.info{"Android $key start sending PCM data, expecting $size bytes"} return } else logcb.accept("PCMFILE_START from Android $key failed, empty zones") } else logcb.accept("PCMFILE_START from Android $key failed, empty filename") } else logcb.accept("PCMFILE_START from Android $key failed, invalid size") cb.accept("PCMFILE_START;NG@") } "PCMFILE_STOP" -> { // stop sending PCM data from Android for paging val pj = listOnGoingPaging[key] if (pj!=null) { listOnGoingPaging.remove(key) tcpreceiver.StopRequestDataFrom(key) // get remaining data val data = pj.GetData() pj.Close() if (data.size==pj.expectedSize){ Logger.info { "Paging job closed from Android $key, total bytes received ${data.size}, writing to file ${pj.filePath.absolutePathString()}" } val result = audioPlayer.WavWriter(data, pj.filePath.absolutePathString(), true) if (result.success) { val qp = QueuePaging( 0u, LocalDateTime.now().format(datetimeformat1), "ANDROID", "PAGING", 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("PCMFILE_STOP;OK@") return } else logcb.accept("Failed to insert paging audio to queue paging table from Android $key, file ${pj.filePath.absolutePathString()}") } else logcb.accept("Failed to write paging audio to file ${pj.filePath.absolutePathString()}, Message : ${result.message}") } else logcb.accept("PCMFILE_STOP from Android $key received size ${data.size} does not match expected ${pj.expectedSize}") } else logcb.accept("PCMFILE_STOP from Android $key failed, no ongoing PCM data receiving") cb.accept("PCMFILE_STOP;NG@") } "STARTPAGINGAND" -> { // Start Paging request from IPM val zones = parts.getOrElse(1) { "" }.replace(",",";") if (ValidString(zones)){ // create pagingjob val pj = PagingJob(key, zones) // masukin ke list listOnGoingPaging[key] = pj Logger.info{"PagingJob created for IPM $key, zones: $zones, file: ${pj.filePath.absolutePathString()}"} // start minta data dari udpreceiver udpreceiver.RequestDataFrom(key){ // push data ke paging job pj.addData(it, it.size) } logcb.accept("Paging started from IPM $key") cb.accept("STARTPAGINGAND;OK@") return } else logcb.accept("Paging start from IPM $key failed, empty zones") cb.accept("STARTPAGINGAND;NG@") } "STOPPAGINGAND" -> { // stop paging request from IPM val pj = listOnGoingPaging[key] if (pj!=null){ listOnGoingPaging.remove(key) udpreceiver.StopRequestDataFrom(key) logcb.accept("Paging stopped from IPM $key") // get remaining data val data = pj.GetData() pj.Close() Logger.info{"Paging job closed from IPM $key, total bytes received ${data.size}, writing to file ${pj.filePath.absolutePathString()}"} val result = audioPlayer.WavWriter(data, pj.filePath.absolutePathString(), true) if (result.success){ val qp = QueuePaging( 0u, LocalDateTime.now().format(datetimeformat1), "IPM", "PAGING", pj.filePath.absolutePathString(), pj.broadcastzones ) if (db.queuepagingDB.Add(qp)){ logcb.accept("Paging audio inserted to queue paging table from IPM $key, file ${pj.filePath.absolutePathString()}") cb.accept("STOPPAGINGAND;OK@") return } else logcb.accept("Failed to insert paging audio to queue paging table from IPM $key, file ${pj.filePath.absolutePathString()}") } else logcb.accept("Failed to write paging audio to file ${pj.filePath.absolutePathString()}, Message : ${result.message}") } else logcb.accept("Paging stop from IPM $key failed, no ongoing paging") cb.accept("STOPPAGINGAND;NG@") } "CANCELPAGINGAND" -> { // cancel paging request from IPM val pj = listOnGoingPaging[key] if (pj!=null){ pj.Close() listOnGoingPaging.remove(key) udpreceiver.StopRequestDataFrom(key) logcb.accept("Paging from IPM $key cancelled") cb.accept("CANCELPAGINGAND;OK@") return } else logcb.accept("Paging cancel from IPM $key failed, no ongoing paging") cb.accept("CANCELPAGINGAND;NG@") } "STARTINITIALIZE" -> { 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){ println("Sending initialization data to $key with username $username") val result = StringBuilder() // kirim Zone result.append("ZONE") userdb.broadcastzones.split(";").map { it.trim() }.filter { it.isNotBlank() }.forEach { result.append(";") result.append(it) } result.append("@") cb.accept(result.toString()) // kirim MSGTOTAL result.clear() val VARMESSAGES = mutableListOf() result.append("MSGTOTAL;") 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() } // 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.ANN_ID } VARMESSAGES.addAll(xx) } result.append(VARMESSAGES.size).append("@") cb.accept(result.toString()) // kirim VARAPTOTAL result.clear() result.append("VARAPTOTAL;") val VARAPTOTAL = mutableListOf() userdb.airline_tags .split(";") .map { it.trim() } .filter { it.isNotBlank() } .forEach { al -> val sb = db.soundDB.List .filter { it.Category.equals(Category.Airline_Name.name, true) } .filter { it.TAG.equals(al, true)} .distinctBy { it.TAG } VARAPTOTAL.addAll(sb) } result.append(VARAPTOTAL.size).append("@") cb.accept(result.toString()) // kirim VARCITYTOTAL result.clear() result.append("VARCITYTOTAL;") val VARCITYTOTAL = mutableListOf() userdb.city_tags .split(";") .map { it.trim() } .filter { it.isNotBlank() } .forEach { ct -> val sb = db.soundDB.List .filter { it.Category.equals(Category.City.name, true) } .filter { it.TAG.equals(ct, true)} .distinctBy { it.TAG } VARCITYTOTAL.addAll(sb) } result.append(VARCITYTOTAL.size).append("@") cb.accept(result.toString()) // kirim VARPLACESTOTAL result.clear() result.append("VARPLACESTOTAL;") val VARPLACESTOTAL = mutableListOf() db.soundDB.List .filter { it.Category.equals(Category.Places.name, true) } .distinctBy { it.TAG } .forEach { VARPLACESTOTAL.add(it) } result.append(VARPLACESTOTAL.size).append("@") cb.accept(result.toString()) // kirim VARSHALATTOTAL result.clear() result.append("VARSHALATTOTAL;") val VARSHALATTOTAL = mutableListOf() db.soundDB.List .filter { it.Category.equals(Category.Shalat.name, true) } .distinctBy { it.TAG } .forEach { VARSHALATTOTAL.add(it) } result.append(VARSHALATTOTAL.size).append("@") cb.accept(result.toString()) // kirim VARSEQUENCETOTAL result.clear() result.append("VARSEQUENCETOTAL;") val VARSEQUENCETOTAL = mutableListOf() db.soundDB.List .filter { it.Category.equals(Category.Sequence.name, true) } .distinctBy { it.TAG } .forEach { VARSEQUENCETOTAL.add(it) } result.append(VARSEQUENCETOTAL.size).append("@") cb.accept(result.toString()) // kirim VARREASONTOTAL result.clear() result.append("VARREASONTOTAL;") val VARREASONTOTAL = mutableListOf() db.soundDB.List .filter { it.Category.equals(Category.Reason.name, true) } .distinctBy { it.TAG } .forEach { VARREASONTOTAL.add(it) } result.append(VARREASONTOTAL.size).append("@") cb.accept(result.toString()) // kirim VARPROCEDURETOTAL val VARPROCEDURETOTAL = mutableListOf() result.clear() result.append("VARPROCEDURETOTAL;") db.soundDB.List .filter { it.Category.equals(Category.Procedure.name, true) } .distinctBy { it.TAG } .forEach { VARPROCEDURETOTAL.add(it) } result.append(VARPROCEDURETOTAL.size).append("@") cb.accept(result.toString()) // kirim VARGATETOTAL val VARGATETOTAL = mutableListOf() result.clear() result.append("VARGATETOTAL;") db.soundDB.List .filter { it.Category.equals(Category.Gate.name, true) } .distinctBy { it.TAG } .forEach { VARGATETOTAL.add(it) } result.append(VARGATETOTAL.size).append("@") cb.accept(result.toString()) // kirim VARCOMPENSATIONTOTAL result.clear() result.append("VARCOMPENSATIONTOTAL;") val VARCOMPENSATIONTOTAL = mutableListOf() db.soundDB.List .filter { it.Category.equals(Category.Compensation.name, true) } .distinctBy { it.TAG } .forEach { VARCOMPENSATIONTOTAL.add(it) } result.append(VARCOMPENSATIONTOTAL.size).append("@") cb.accept(result.toString()) // kirim VARGREETINGTOTAL result.clear() result.append("VARGREETINGTOTAL;") val VARGREETINGTOTAL = mutableListOf() db.soundDB.List .filter { it.Category.equals(Category.Greeting.name, true) } .distinctBy { it.TAG } .forEach { VARGREETINGTOTAL.add(it) } result.append(VARGREETINGTOTAL.size).append("@") cb.accept(result.toString()) //Append MSG, for Android only Indonesia and English if (VARMESSAGES.isNotEmpty()) { result.clear() VARMESSAGES.forEachIndexed { index, msg -> val ann_id = msg.ANN_ID val msg_indo = db.messageDB.List.find { it.ANN_ID == ann_id && it.Language.equals( Language.INDONESIA.name, true ) } val msg_eng = db.messageDB.List.find { it.ANN_ID == ann_id && it.Language.equals( Language.ENGLISH.name, true ) } val description = msg_indo?.Description ?: msg_eng?.Description ?: "UNKNOWN" result.append("MSG;$index;$ann_id;$description;") result.append(msg_indo?.Message_Detail ?:"").append(";") result.append(msg_eng?.Message_Detail ?:"").append("@") } cb.accept(result.toString()) } // append VARAP if (VARAPTOTAL.isNotEmpty()) { result.clear() VARAPTOTAL.forEachIndexed { index, sb -> result.append("VARAP;$index;${sb.TAG};${sb.Description}@") } cb.accept(result.toString()) } // append VARCITY if (VARCITYTOTAL.isNotEmpty()) { result.clear() VARCITYTOTAL.forEachIndexed { index, sb -> result.append("VARCITY;$index;${sb.TAG};${sb.Description}@") } cb.accept(result.toString()) } // append VARPLACES if (VARPLACESTOTAL.isNotEmpty()) { result.clear() VARPLACESTOTAL.forEachIndexed { index, sb -> result.append("VARPLACES;$index;${sb.TAG};${sb.Description}@") } cb.accept(result.toString()) } // append VARSHALAT if (VARSHALATTOTAL.isNotEmpty()) { result.clear() VARSHALATTOTAL.forEachIndexed { index, sb -> result.append("VARSHALAT;$index;${sb.TAG};${sb.Description}@") } cb.accept(result.toString()) } // append VARSEQUENCE if (VARSEQUENCETOTAL.isNotEmpty()) { result.clear() VARSEQUENCETOTAL.forEachIndexed { index, sb -> result.append("VARSEQUENCE;$index;${sb.TAG};${sb.Description}@") } cb.accept(result.toString()) } // append VARREASON if (VARREASONTOTAL.isNotEmpty()) { result.clear() VARREASONTOTAL.forEachIndexed { index, sb -> result.append("VARREASON;$index;${sb.TAG};${sb.Description}@") } cb.accept(result.toString()) } // append VARPROCEDURE if (VARPROCEDURETOTAL.isNotEmpty()) { result.clear() VARPROCEDURETOTAL.forEachIndexed { index, sb -> result.append("VARPROCEDURE;$index;${sb.TAG};${sb.Description}@") } cb.accept(result.toString()) } // append VARGATE if (VARGATETOTAL.isNotEmpty()) { result.clear() VARGATETOTAL.forEachIndexed { index, sb -> result.append("VARGATE;$index;${sb.TAG};${sb.Description}@") } cb.accept(result.toString()) } // append VARCOMPENSATION if (VARCOMPENSATIONTOTAL.isNotEmpty()) { result.clear() VARCOMPENSATIONTOTAL.forEachIndexed { index, sb -> result.append("VARCOMPENSATION;$index;${sb.TAG};${sb.Description}@") } cb.accept(result.toString()) } // append VARGREETING if (VARGREETINGTOTAL.isNotEmpty()) { result.clear() VARGREETINGTOTAL.forEachIndexed { index, sb -> result.append("VARGREETING;$index;${sb.TAG};${sb.Description}@") } 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) { "" } // language bisa lebih dari satu, dipisah dengan koma val lang = parts.getOrElse(2) { "" }.replace(",",";") // tags bisa lebih dari satu, dipisah dengan spasi val tags = parts.getOrElse(3) { "" }.replace(",",";") // zone bisa lebih dari satu, dipisah dengan koma 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("BROADCASTAND;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 } }