434 lines
22 KiB
Kotlin
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
|
|
}
|
|
} |