Files
AAS_NewGeneration/src/commandServer/TCP_Android_Command_Server.kt
2025-10-06 11:06:32 +07:00

434 lines
22 KiB
Kotlin

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<String, Socket>()
lateinit var logcb: Consumer<String>
private val listUserLogin = mutableListOf<userLogin>()
private val listOnGoingPaging = mutableMapOf<String, PagingJob>()
/**
* 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<String>): 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<String>) {
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<Messagebank>()
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<Soundbank>()
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<Soundbank>()
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<Soundbank>()
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<Soundbank>()
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<Soundbank>()
sb_split.forEach {
val sb = db.Find_Soundbank_Sequence(it).firstOrNull()
if (sb != null) VARSEQUENCETOTAL.add(sb)
}
// VAR REASON TOTAL
val VARREASONTOTAL = mutableListOf<Soundbank>()
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<Soundbank>()
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
}
}