From 1e7adeba25f52487d825d00e00184df224933ebd Mon Sep 17 00:00:00 2001 From: rdkartono Date: Thu, 2 Oct 2025 13:17:52 +0700 Subject: [PATCH] commit 02/10/2025 --- src/Main.kt | 11 +- src/audio/AudioPlayer.kt | 8 + src/audio/UDPReceiver.kt | 79 +++++++++ src/commandServer/PagingJob.kt | 45 +++++ .../TCP_Android_Command_Server.kt | 154 ++++++++++++++++-- 5 files changed, 278 insertions(+), 19 deletions(-) create mode 100644 src/audio/UDPReceiver.kt create mode 100644 src/commandServer/PagingJob.kt diff --git a/src/Main.kt b/src/Main.kt index bd32445..9659b97 100644 --- a/src/Main.kt +++ b/src/Main.kt @@ -1,4 +1,5 @@ import audio.AudioPlayer +import audio.UDPReceiver import barix.BarixConnection import barix.TCP_Barix_Command_Server import com.sun.jna.Platform @@ -20,6 +21,7 @@ import kotlin.concurrent.fixedRateTimer lateinit var db: MariaDB lateinit var audioPlayer: AudioPlayer val StreamerOutputs: MutableMap = HashMap() +lateinit var udpreceiver: UDPReceiver const val version = "0.0.2 (23/09/2025)" // dipakai untuk pilih voice type, bisa diganti via web nanti @@ -74,10 +76,16 @@ fun main() { )) web.Start() + udpreceiver = UDPReceiver() + if (udpreceiver.Start()) { + Logger.info { "UDP Receiver started on port 5002" } + } else { + Logger.error { "Failed to start UDP Receiver on port 5002" } + } + val androidserver = TCP_Android_Command_Server() androidserver.StartTcpServer(5003){ Logger.info { it } - db.logDB.Add(Log.NewLog("ANDROID", it)) } @@ -123,6 +131,7 @@ fun main() { androidserver.StopTcpCommand() onlinechecker.cancel() web.Stop() + udpreceiver.Stop() audioPlayer.Close() db.close() Logger.info { "All services stopped, exiting application." } diff --git a/src/audio/AudioPlayer.kt b/src/audio/AudioPlayer.kt index a70d3fe..af96e6c 100644 --- a/src/audio/AudioPlayer.kt +++ b/src/audio/AudioPlayer.kt @@ -126,6 +126,14 @@ class AudioPlayer (var samplingrate: Int) { return result } + fun WavWriter(data: ByteArray, target: String, callback: BiConsumer) { + val source = AudioFileInfo() + source.bytes = data + source.fileName = "In-Memory Data" + val sources = listOf(source) + WavWriter(sources, target, callback) + } + /** * Writes the audio data from the sources to a WAV file. * @param sources List of AudioFileInfo objects containing the audio data to write. diff --git a/src/audio/UDPReceiver.kt b/src/audio/UDPReceiver.kt new file mode 100644 index 0000000..2cc714e --- /dev/null +++ b/src/audio/UDPReceiver.kt @@ -0,0 +1,79 @@ +package audio + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.net.DatagramSocket +import java.util.function.Consumer + +@Suppress("unused") +/** + * UDPReceiver is a class that listens for UDP packets on a specified port. + * It is designed to run in a separate thread and can be stopped when no longer needed. + * @param portnumber The port to listen for incoming UDP packets (default is 5002 + */ +class UDPReceiver(val portnumber: Int = 5002) { + private lateinit var socket : DatagramSocket + private var isRunning = false + private val dataCallback = mutableMapOf>() + + /** + * Start listening for UDP packets on the specified port. + * @return true if successful, false otherwise + */ + fun Start() : Boolean{ + return try { + socket = DatagramSocket(portnumber) + isRunning = true + CoroutineScope(Dispatchers.IO).launch { + while(isRunning){ + try { + val buffer = ByteArray(2048) + val packet = java.net.DatagramPacket(buffer, buffer.size) + socket.receive(packet) + val data = ByteArray(packet.length) + System.arraycopy(packet.data, 0, data, 0, packet.length) + dataCallback[packet.address.hostAddress].let { + it?.accept(data) + } + + } catch (e: Exception) { + if (isRunning) { + println("Error receiving UDP packet: ${e.message}") + } + } + } + } + true + } catch (e: Exception) { + false + } + } + + /** + * Register a callback function to be called when data is received from the specified IP address. + * @param ipaddress The IP address to listen for incoming UDP packets. + * @param callback A callback function that will be called when data is received from the specified IP address. + */ + fun RequestDataFrom(ipaddress: String, callback: Consumer){ + dataCallback[ipaddress] = callback + } + + /** + * Unregister the callback function for the specified IP address. + * @param ipaddress The IP address to stop listening for incoming UDP packets. + */ + fun StopRequestDataFrom(ipaddress: String){ + dataCallback.remove(ipaddress) + } + + /** + * Stop listening for UDP packets and close the socket. + */ + fun Stop(){ + if (isRunning){ + isRunning = false + socket.close() + } + } +} \ No newline at end of file diff --git a/src/commandServer/PagingJob.kt b/src/commandServer/PagingJob.kt new file mode 100644 index 0000000..3a39a84 --- /dev/null +++ b/src/commandServer/PagingJob.kt @@ -0,0 +1,45 @@ +package commandServer + +import codes.Somecodes.Companion.PagingResult_directory +import codes.Somecodes.Companion.filenameformat +import org.tinylog.Logger +import java.io.ByteArrayOutputStream +import java.nio.file.Path +import java.time.LocalDateTime + +/** + * Class to handle a paging job, storing incoming audio data and metadata. + * @param fromIP The IP address from which the paging data is received. + * @param broadcastzones The zones to which the paging is broadcasted, is a semicolon-separated string. + */ +class PagingJob(val fromIP: String, val broadcastzones: String) { + val filePath : Path = PagingResult_directory.resolve(LocalDateTime.now().format(filenameformat)+"_RAW.wav") + private val bos : ByteArrayOutputStream = ByteArrayOutputStream() + var totalBytesReceived = 0; private set + var isRunning = true; private set + + + /** + * Adds incoming audio data to the job. + * @param data The byte array containing audio data. + * @param length The number of bytes to write from the data array. + */ + fun addData(data: ByteArray, length: Int) { + Logger.info{"PagingJob from $fromIP, zones: $broadcastzones, received $length bytes"} + bos.write(data, 0, length) + totalBytesReceived += length + } + + /** + * Retrieves the accumulated audio data as a byte array. + * @return A byte array containing all received audio data. + */ + fun GetData(): ByteArray { + return bos.toByteArray() + } + + fun Close(){ + bos.close() + isRunning = false + } +} \ No newline at end of file diff --git a/src/commandServer/TCP_Android_Command_Server.kt b/src/commandServer/TCP_Android_Command_Server.kt index 95eabd1..6597cfa 100644 --- a/src/commandServer/TCP_Android_Command_Server.kt +++ b/src/commandServer/TCP_Android_Command_Server.kt @@ -1,7 +1,12 @@ 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 @@ -11,11 +16,14 @@ 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 { @@ -24,6 +32,7 @@ class TCP_Android_Command_Server { private val socketMap = mutableMapOf() lateinit var logcb: Consumer private val listUserLogin = mutableListOf() + private val listOnGoingPaging = mutableMapOf() /** * Start TCP Command Server @@ -45,7 +54,8 @@ class TCP_Android_Command_Server { { CoroutineScope(Dispatchers.Main).launch { if (socket != null) { - val key: String = socket.inetAddress.hostAddress + ":" + socket.port + // key is IP address only + val key: String = socket.inetAddress.hostAddress socketMap[key] = socket Logger.info { "Start communicating with $key" } socket.getInputStream().let { din -> @@ -110,23 +120,31 @@ class TCP_Android_Command_Server { 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}) { - cb.accept("LOGIN;TRUE@") 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@") @@ -137,27 +155,85 @@ class TCP_Android_Command_Server { } } - "PCMFILE_START" -> { - // TODO read coding here + "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" -> { - // TODO read coding here - } + "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@") + } - "STARTPAGINGAND" -> { - // TODO read coding here - } + } 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@") + } - "STOPPAGINGAND" -> { - // TODO read coding here } "CANCELPAGINGAND" -> { - // TODO read coding here + 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 } @@ -183,8 +259,11 @@ class TCP_Android_Command_Server { .filter { xx -> xx.all{it.isDigit()} } // iterasi setiap ANN_ID .forEach { annid -> - // masukin ke VARMESSAGES yang unik secara ANN_ID dan Description - VARMESSAGES.addAll(db.messageDB.List.distinctBy { it.ANN_ID }.distinctBy { it.Description }) + // 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 @@ -240,7 +319,16 @@ class TCP_Android_Command_Server { cb.accept(result.toString()) result.clear() - //TODO append MSG + + //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== Language.INDONESIA.name }?.let {result.append(it.Message_Detail)} ?: result.append("NA") + result.append(";") + value.find {it.Language== Language.ENGLISH.name }?.let {result.append(it.Message_Detail)} ?: result.append("NA") + result.append("@") + } // append VARAP VARAPTOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank -> @@ -274,7 +362,7 @@ class TCP_Android_Command_Server { 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") @@ -282,7 +370,37 @@ class TCP_Android_Command_Server { } "BROADCASTAND" -> { - // TODO read coding here + // 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 -> {