commit 02/10/2025

This commit is contained in:
2025-10-02 13:17:52 +07:00
parent 83a6ee9fd0
commit 1e7adeba25
5 changed files with 278 additions and 19 deletions

View File

@@ -1,4 +1,5 @@
import audio.AudioPlayer import audio.AudioPlayer
import audio.UDPReceiver
import barix.BarixConnection import barix.BarixConnection
import barix.TCP_Barix_Command_Server import barix.TCP_Barix_Command_Server
import com.sun.jna.Platform import com.sun.jna.Platform
@@ -20,6 +21,7 @@ import kotlin.concurrent.fixedRateTimer
lateinit var db: MariaDB lateinit var db: MariaDB
lateinit var audioPlayer: AudioPlayer lateinit var audioPlayer: AudioPlayer
val StreamerOutputs: MutableMap<String, BarixConnection> = HashMap() val StreamerOutputs: MutableMap<String, BarixConnection> = HashMap()
lateinit var udpreceiver: UDPReceiver
const val version = "0.0.2 (23/09/2025)" const val version = "0.0.2 (23/09/2025)"
// dipakai untuk pilih voice type, bisa diganti via web nanti // dipakai untuk pilih voice type, bisa diganti via web nanti
@@ -74,10 +76,16 @@ fun main() {
)) ))
web.Start() 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() val androidserver = TCP_Android_Command_Server()
androidserver.StartTcpServer(5003){ androidserver.StartTcpServer(5003){
Logger.info { it } Logger.info { it }
db.logDB.Add(Log.NewLog("ANDROID", it)) db.logDB.Add(Log.NewLog("ANDROID", it))
} }
@@ -123,6 +131,7 @@ fun main() {
androidserver.StopTcpCommand() androidserver.StopTcpCommand()
onlinechecker.cancel() onlinechecker.cancel()
web.Stop() web.Stop()
udpreceiver.Stop()
audioPlayer.Close() audioPlayer.Close()
db.close() db.close()
Logger.info { "All services stopped, exiting application." } Logger.info { "All services stopped, exiting application." }

View File

@@ -126,6 +126,14 @@ class AudioPlayer (var samplingrate: Int) {
return result return result
} }
fun WavWriter(data: ByteArray, target: String, callback: BiConsumer<Boolean, String>) {
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. * Writes the audio data from the sources to a WAV file.
* @param sources List of AudioFileInfo objects containing the audio data to write. * @param sources List of AudioFileInfo objects containing the audio data to write.

79
src/audio/UDPReceiver.kt Normal file
View File

@@ -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<String, Consumer<ByteArray>>()
/**
* 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<ByteArray>){
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()
}
}
}

View File

@@ -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
}
}

View File

@@ -1,7 +1,12 @@
package commandServer package commandServer
import audioPlayer
import codes.Somecodes.Companion.ValidString import codes.Somecodes.Companion.ValidString
import codes.Somecodes.Companion.datetimeformat1
import content.Language
import database.Messagebank import database.Messagebank
import database.QueuePaging
import database.QueueTable
import database.Soundbank import database.Soundbank
import db import db
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -11,11 +16,14 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.tinylog.Logger import org.tinylog.Logger
import udpreceiver
import java.net.ServerSocket import java.net.ServerSocket
import java.net.Socket import java.net.Socket
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
import java.time.LocalDateTime
import java.util.function.Consumer import java.util.function.Consumer
import kotlin.io.path.absolutePathString
@Suppress("unused") @Suppress("unused")
class TCP_Android_Command_Server { class TCP_Android_Command_Server {
@@ -24,6 +32,7 @@ class TCP_Android_Command_Server {
private val socketMap = mutableMapOf<String, Socket>() private val socketMap = mutableMapOf<String, Socket>()
lateinit var logcb: Consumer<String> lateinit var logcb: Consumer<String>
private val listUserLogin = mutableListOf<userLogin>() private val listUserLogin = mutableListOf<userLogin>()
private val listOnGoingPaging = mutableMapOf<String, PagingJob>()
/** /**
* Start TCP Command Server * Start TCP Command Server
@@ -45,7 +54,8 @@ class TCP_Android_Command_Server {
{ {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
if (socket != null) { 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 socketMap[key] = socket
Logger.info { "Start communicating with $key" } Logger.info { "Start communicating with $key" }
socket.getInputStream().let { din -> socket.getInputStream().let { din ->
@@ -110,23 +120,31 @@ class TCP_Android_Command_Server {
return ByteArray(0) 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>) { private fun process_command(key: String, cmd: String, cb: Consumer<String>) {
Logger.info { "Command from $key : $cmd" } Logger.info { "Command from $key : $cmd" }
val parts = cmd.split(";").map { it.trim() }.filter { it.isNotBlank() }.map { it.uppercase() } val parts = cmd.split(";").map { it.trim() }.filter { it.isNotBlank() }.map { it.uppercase() }
when (parts[0]) { when (parts[0]) {
"GETLOGIN" -> { "GETLOGIN" -> {
// Android login request
val username = parts.getOrElse(1) { "" } val username = parts.getOrElse(1) { "" }
val password = parts.getOrElse(2) { "" } val password = parts.getOrElse(2) { "" }
if (ValidString(username) && ValidString(password)) { if (ValidString(username) && ValidString(password)) {
if (db.userDB.List.any{it.username==username && it.password==password}) { if (db.userDB.List.any{it.username==username && it.password==password}) {
cb.accept("LOGIN;TRUE@")
val existing = listUserLogin.find { it.ip == key} val existing = listUserLogin.find { it.ip == key}
if (existing!=null){ if (existing!=null){
existing.username = username existing.username = username
} else{ } else{
listUserLogin.add(userLogin(key, username)) listUserLogin.add(userLogin(key, username))
} }
cb.accept("LOGIN;TRUE@")
logcb.accept("Android Login success from $key as $username") logcb.accept("Android Login success from $key as $username")
return
} else { } else {
logcb.accept("Android Login failed from $key as $username") logcb.accept("Android Login failed from $key as $username")
cb.accept("LOGIN;FALSE@") cb.accept("LOGIN;FALSE@")
@@ -137,27 +155,85 @@ class TCP_Android_Command_Server {
} }
} }
"PCMFILE_START" -> { "PCMFILE_START","STARTPAGINGAND" -> {
// TODO read coding here 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" -> { "PCMFILE_STOP","STOPPAGINGAND" -> {
// TODO read coding here 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" -> { } else {
// TODO read coding here 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" -> { "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" -> { "STARTINITIALIZE" -> {
// pengiriman variabel ke Android
val username = parts.getOrElse(1) { "" } val username = parts.getOrElse(1) { "" }
if (ValidString(username)){ if (ValidString(username)){
val userlogin = listUserLogin.find { it.username == username } val userlogin = listUserLogin.find { it.username == username }
@@ -183,8 +259,11 @@ class TCP_Android_Command_Server {
.filter { xx -> xx.all{it.isDigit()} } .filter { xx -> xx.all{it.isDigit()} }
// iterasi setiap ANN_ID // iterasi setiap ANN_ID
.forEach { annid -> .forEach { annid ->
// masukin ke VARMESSAGES yang unik secara ANN_ID dan Description // masukin ke VARMESSAGES yang unik secara ANN_ID dan Language
VARMESSAGES.addAll(db.messageDB.List.distinctBy { it.ANN_ID }.distinctBy { it.Description }) val xx = db.messageDB.List
.filter{ it.ANN_ID == annid.toUInt() }
.distinctBy { it.Language }
VARMESSAGES.addAll(xx)
} }
result.append("MSGTOTAL;").append(VARMESSAGES.size).append("@") result.append("MSGTOTAL;").append(VARMESSAGES.size).append("@")
// VAR AP TOTAL // VAR AP TOTAL
@@ -240,7 +319,16 @@ class TCP_Android_Command_Server {
cb.accept(result.toString()) cb.accept(result.toString())
result.clear() 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 // append VARAP
VARAPTOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank -> VARAPTOTAL.distinctBy { it.Description }.forEachIndexed { index, soundbank ->
@@ -274,7 +362,7 @@ class TCP_Android_Command_Server {
cb.accept(result.toString()) cb.accept(result.toString())
logcb.accept("All variables sent to $key with username $username") 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 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 unregistered username $username")
} else logcb.accept("STARTINITIALIZE failed from $key with empty username") } else logcb.accept("STARTINITIALIZE failed from $key with empty username")
@@ -282,7 +370,37 @@ class TCP_Android_Command_Server {
} }
"BROADCASTAND" -> { "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 -> { else -> {