commit 07/02/2026

This commit is contained in:
2026-02-07 17:23:41 +07:00
parent c8f7f35c79
commit 1790852242
13 changed files with 5223 additions and 1934 deletions

View File

@@ -58,10 +58,6 @@
margin-right: .5rem!important;
}
.mb-2 {
margin-bottom: .5rem!important;
}
.mb-3 {
margin-bottom: 1rem!important;
}

View File

@@ -25,22 +25,38 @@
* @property {JQuery<HTMLElement> | null} ip - The jQuery result should be <h6> element.
* @property {JQuery<HTMLElement> | null} buffer - The jQuery result should be <h6> element.
* @property {JQuery<HTMLElement> | null} status - The jQuery result should be <p> element.
* @property {JQuery<HTMLElement> | null} filename - The jQuery result should be <h6> element.
* @property {JQuery<HTMLElement> | null} duration - The jQuery result should be <h6> element.
* @property {JQuery<HTMLElement> | null} elapsed - The jQuery result should be <h6> element.
* @property {JQuery<HTMLElement> | null} broadcastzones - The jQuery result should be <h6> element.
* @property {JQuery<HTMLElement> | null} vu - The jQuery result should be <progress-bar> element.
*/
function getCardByIndex(index) {
let cardname = "ch" + index.toString().padStart(2, '0');
let obj = {
// title is <h4> element wiht id `streamertitle${index}`, with index as two digit number, e.g. 01, 02, 03
title: $(`#streamertitle${index.toString().padStart(2, '0')}`),
//title: $(`#streamertitle${index.toString().padStart(2, '0')}`),
// ip is <h6> element with id `streamerip${index}`, with index as two digit number, e.g. 01, 02, 03
ip: $(`#streamerip${index.toString().padStart(2, '0')}`),
//ip: $(`#streamerip${index.toString().padStart(2, '0')}`),
// buffer is <h6> element with id `streamerbuffer${index}`, with index as two digit number, e.g. 01, 02, 03
buffer: $(`#streamerbuffer${index.toString().padStart(2, '0')}`),
//buffer: $(`#streamerbuffer${index.toString().padStart(2, '0')}`),
// status is <p> element with id `streamerstatus${index}`, with index as two digit number, e.g. 01, 02, 03
status: $(`#streamerstatus${index.toString().padStart(2, '0')}`),
//status: $(`#streamerstatus${index.toString().padStart(2, '0')}`),
// vu is <progress-bar> element with id `streamervu${index}`, with index as two digit number, e.g. 01, 02, 03
vu: $(`#streamervu${index.toString().padStart(2, '0')} .progress-bar`),
//vu: $(`#streamervu${index.toString().padStart(2, '0')} .progress-bar`),
card: $(`#${cardname}`),
title: $(`#${cardname} .streamertitle`),
ip: $(`#${cardname} .streamerip`),
buffer: $(`#${cardname} .streamerbuffer`),
status: $(`#${cardname} .streamerstatus`),
vu: $(`#${cardname} .streamervu .progress-bar`),
filename: $(`#${cardname} .streamerfile`),
duration: $(`#${cardname} .streamerduration`),
elapsed: $(`#${cardname} .streamerelapsed`),
broadcastzones: $(`#${cardname} .streamerzones`),
}
return obj;
}
@@ -64,29 +80,71 @@ function UpdateStreamerCard(values) {
values = [];
}
let visiblilitychanged = false;
for (let i = 1; i <= 64; i++) {
let vv = values.find(v => v.index === i);
let card = getCardByIndex(i);
const vv = values.find(v => v.index === i);
const cardname = "ch" + i.toString().padStart(2, '0');
const $card = $(`#${cardname}`);
if (vv) {
// there is value for this index
if (card.title) card.title.text(vv.channel ? vv.channel : `Channel ${i.toString().padStart(2, '0')}`);
if (card.ip) card.ip.text(`IP Address: ${vv.ipaddress ? vv.ipaddress : 'N/A'}`);
if (card.buffer) card.buffer.text(`Buffer: ${vv.bufferRemain !== undefined && vv.bufferRemain !== null ? vv.bufferRemain.toString() : 'N/A'}`);
if (card.status) card.status.text(`Status: ${vv.isPlaying ? 'Playing' : 'Idle'}`);
if (card.vu) {
setProgress(i, card.vu, vv.vu, 100);
// ada data untuk index i
if ($card.length > 0) {
// ada card untuk index i, show card
if ($card.hasClass('d-none')) {
visiblilitychanged = true;
$card.removeClass('d-none');
$card.closest('.streamercol').removeClass('d-none'); // show the column as well
}
const $title = $(`#${cardname} .streamertitle`);
const $ip = $(`#${cardname} .streamerip`);
const $buffer = $(`#${cardname} .streamerbuffer`);
const $status = $(`#${cardname} .streamerstatus`);
const $vu = $(`#${cardname} .streamervu .progress-bar`);
const $filename = $(`#${cardname} .streamerfile`);
const $duration = $(`#${cardname} .streamerduration`);
const $elapsed = $(`#${cardname} .streamerelapsed`);
const $broadcastzones = $(`#${cardname} .streamerzones`);
//console.log(`Updating card for index ${i}`, vv);
$title.text(vv.channel ? vv.channel : `Channel ${i.toString().padStart(2, '0')}`);
$ip.text(vv.ipaddress ? vv.ipaddress : 'N/A');
$buffer.text(vv.bufferRemain !== undefined && vv.bufferRemain !== null ? vv.bufferRemain.toString() : 'N/A');
$status.text(vv.isPlaying ? 'Playing' : 'Idle');
setProgress(i, $vu, vv.vu ? vv.vu : 0, 100);
$filename.text(vv.filename ? vv.filename : 'N/A');
$duration.text(vv.duration ? vv.duration : 'N/A');
$elapsed.text(vv.elapsed ? vv.elapsed : 'N/A');
$broadcastzones.text(vv.broadcastzones ? vv.broadcastzones : 'N/A');
}
} else {
// no value for this index, disable the card
if (card.title) card.title.text(`Channel ${i.toString().padStart(2, '0')}`);
if (card.ip) card.ip.text(`IP Address: N/A`);
if (card.buffer) card.buffer.text(`Buffer: N/A`);
if (card.status) card.status.text(`Status: Disconnected`);
if (card.vu) {
setProgress(i, card.vu, 0, 100);
// tidak ada data untuk index i, hide card
if ($card.length > 0) {
// ada card untuk index i, hide card
if (!$card.hasClass('d-none')) {
visiblilitychanged = true;
$card.addClass('d-none');
$card.closest('.streamercol').addClass('d-none'); // hide the column as well
}
}
}
}
// hide rows that have all cards hidden
if (visiblilitychanged) {
$('.streamerrow').each(function () {
const $row = $(this);
const visiblecards = $row.find('.streamercard:not(.d-none)');
if (visiblecards.length === 0) {
$row.addClass('d-none');
} else {
$row.removeClass('d-none');
}
});
}
}
/**
@@ -428,51 +486,51 @@ $(document).ready(function () {
runIntervalJob();
window.addEventListener('ws_connected', () =>{
window.addEventListener('ws_connected', () => {
console.log("overview.js ws_connected event triggered");
runIntervalJob();
runIntervalJob();
});
window.addEventListener('ws_disconnected', ()=>{
window.addEventListener('ws_disconnected', () => {
console.log("overview.js ws_disconnected event triggered");
if (intervaljob1) clearInterval(intervaljob1);
if (intervaljob2) clearInterval(intervaljob2);
intervaljob1 = null;
intervaljob2 = null;
if (intervaljob1) clearInterval(intervaljob1);
if (intervaljob2) clearInterval(intervaljob2);
intervaljob1 = null;
intervaljob2 = null;
});
window.addEventListener('ws_message', ()=>{
window.addEventListener('ws_message', () => {
let rep = event.detail;
let cmd = rep.reply;
let data = rep.data;
if (cmd && cmd.length > 0) {
switch (cmd) {
case "getPagingQueue":
let pq = JSON.parse(data);
window.PagingQueue = [];
if (Array.isArray(pq) && pq.length > 0) {
window.PagingQueue.push(...pq);
}
fill_pagingqueuetablebody(window.PagingQueue);
break;
case "getAASQueue":
let aq = JSON.parse(data);
window.QueueTable = [];
if (Array.isArray(aq) && aq.length > 0) {
window.QueueTable.push(...aq);
}
fill_automaticqueuetablebody(window.QueueTable);
break;
case "getStreamerOutputs":
/**
* @type {StreamerOutputData[]}
*/
let so = JSON.parse(data);
UpdateStreamerCard(so);
break;
}
let cmd = rep.reply;
let data = rep.data;
if (cmd && cmd.length > 0) {
switch (cmd) {
case "getPagingQueue":
let pq = JSON.parse(data);
window.PagingQueue = [];
if (Array.isArray(pq) && pq.length > 0) {
window.PagingQueue.push(...pq);
}
fill_pagingqueuetablebody(window.PagingQueue);
break;
case "getAASQueue":
let aq = JSON.parse(data);
window.QueueTable = [];
if (Array.isArray(aq) && aq.length > 0) {
window.QueueTable.push(...aq);
}
fill_automaticqueuetablebody(window.QueueTable);
break;
case "getStreamerOutputs":
/**
* @type {StreamerOutputData[]}
*/
let so = JSON.parse(data);
UpdateStreamerCard(so);
break;
}
}
});
$(window).on('beforeunload', function () {

File diff suppressed because it is too large Load Diff

View File

@@ -20,20 +20,72 @@
</head>
<body>
<div class="card" id="streamercard">
<div class="card streamercard" id="streamercard">
<div class="card-body card-channel">
<h4 class="card-title" id="streamertitle">Channel 01</h4>
<h4 class="card-title streamertitle" id="streamertitle">Channel 01</h4>
<div class="row">
<div class="col-12 col-sm-12 col-md-12 col-lg-6 col-xl-6 col-xxl-6">
<h6 class="text-muted mb-2" id="streamerip">IP :&nbsp;192.168.10.10</h6>
<div class="col-3">
<p class="w-100 h-100 align-content-center">IP</p>
</div>
<div class="col-12 col-sm-12 col-md-12 col-lg-6 col-xl-6 col-xxl-6">
<h6 class="text-muted mb-2" id="streamerbuffer">Free : 64KB</h6>
<div class="col">
<p class="w-100 h-100 align-content-center streamerip" id="streamerip">N/A</p>
</div>
</div>
<p class="card-text" id="streamerstatus">Status : Idle</p>
<div class="progress" id="streamervu">
<div class="progress-bar" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" style="width: 50%;">50%</div>
<div class="row">
<div class="col-3">
<p class="w-100 h-100 align-content-center">Buffer</p>
</div>
<div class="col">
<p class="w-100 h-100 align-content-center streamerbuffer" id="streamerbuffer">N/A</p>
</div>
</div>
<div class="row">
<div class="col-3">
<p class="w-100 h-100 align-content-center">Status</p>
</div>
<div class="col">
<p class="w-100 h-100 align-content-center streamerstatus" id="streamerstatus">N/A</p>
</div>
</div>
<div class="row">
<div class="col-3">
<p class="w-100 h-100 align-content-center">File</p>
</div>
<div class="col">
<p class="w-100 h-100 align-content-center streamerfile" id="streamerfile">N/A</p>
</div>
</div>
<div class="row">
<div class="col-3">
<p class="w-100 h-100 align-content-center">Zones</p>
</div>
<div class="col">
<p class="w-100 h-100 align-content-center streamerzones" id="streamerzones">N/A</p>
</div>
</div>
<div class="row">
<div class="col-3">
<p class="w-100 h-100 align-content-center">Duration</p>
</div>
<div class="col">
<p class="w-100 h-100 align-content-center streamerduration" id="streamerduration">N/A</p>
</div>
<div class="col-3">
<p class="w-100 h-100 align-content-center">Elapsed</p>
</div>
<div class="col">
<p class="w-100 h-100 align-content-center streamerelapsed" id="streamerelapsed">N/A</p>
</div>
</div>
<div class="row">
<div class="col-3">
<p class="w-100 h-100 align-content-center">VU</p>
</div>
<div class="col">
<div class="progress w-100 h-50 streamervu" id="streamervu">
<div class="progress-bar" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" style="width: 50%;">50%</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -177,13 +177,29 @@ fun main(args: Array<String>) {
while (isActive) {
delay(1000)
// prioritas 1 , habisin queue paging
subcode01.Read_Queue_Paging()
if (subcode01.Read_Queue_Paging()){
// processing paging, skip selanjutnya
delay(2000)
continue
}
// prioritas 2, habisin queue shalat
subcode01.Read_Queue_Shalat()
if (subcode01.Read_Queue_Shalat()){
// processing shalat, skip selanjutnya
delay(2000)
continue
}
// prioritas 3, habisin queue timer
subcode01.Read_Queue_Timer()
if (subcode01.Read_Queue_Timer()){
// processing timer, skip selanjutnya
delay(2000)
continue
}
// prioritas 4, habisin queue soundbank
subcode01.Read_Queue_Soundbank()
if (subcode01.Read_Queue_Soundbank()){
// processing soundbank, skip selanjutnya
delay(2000)
continue
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -18,4 +18,39 @@ class AudioFileInfo {
fun isValid() : Boolean {
return fileName.isNotBlank() && fileSize > 0 && duration > 0.0 && bytes.isNotEmpty()
}
/**
* Convert the duration to a human-readable string format.
* @return Duration as a string in HH:MM:SS or MM:SS format or SS if less than a minute.
*/
fun DurationToString() : String {
val totalSeconds = duration.toInt()
val hours = totalSeconds / 3600
val minutes = (totalSeconds % 3600) / 60
val seconds = totalSeconds % 60
return when {
hours > 0 -> String.format("%02d h:%02d m:%02d s", hours, minutes, seconds)
minutes > 0 -> String.format("%02d m:%02d s", minutes, seconds)
else -> String.format("00:%02d s", seconds)
}
}
/**
* Convert the file size to a human-readable string format.
* @return File size as a string in Bytes, KB, MB, or GB.
*/
fun FileSizeToString() : String {
val kb = 1024
val mb = kb * 1024
val gb = mb * 1024
return when {
fileSize >= gb -> String.format("%.2f GB", fileSize.toDouble() / gb)
fileSize >= mb -> String.format("%.2f MB", fileSize.toDouble() / mb)
fileSize >= kb -> String.format("%.2f KB", fileSize.toDouble() / kb)
else -> "$fileSize Bytes"
}
}
}

View File

@@ -1,8 +1,7 @@
package barix
import audio.AudioFileInfo
import audio.Mp3Encoder
import codes.Somecodes
import com.fasterxml.jackson.databind.JsonNode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -16,7 +15,6 @@ import java.nio.ByteBuffer
import java.util.function.Consumer
import kotlin.experimental.or
@Suppress("unused")
class BarixConnection(val index: UInt, var channel: String, val ipaddress: String, val port: Int = 5002) : AutoCloseable {
private var _bR: Int = 0
private var _sd: Int = 0
@@ -29,6 +27,89 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
private val mp3Consumer = mutableMapOf<String, Consumer<ByteArray>>()
private val udp = DatagramSocket()
private var _barixmode: Boolean = false
private var _usedbybroadcastzone = mutableSetOf<String>()
private var afi : AudioFileInfo? = null
private var starttick: Long? = null
/**
* Set audio file information used for playback
* @param value The AudioFileInfo object containing audio file details
*/
fun SetAudioFileInfo(value: AudioFileInfo?){
afi = value
}
/**
* Get audio file information used for playback
* @return The AudioFileInfo object containing audio file details, or null if not set
*/
fun GetAudioFileInfo() : AudioFileInfo?{
return afi
}
/**
* Set the start tick for playback timing
* @param tick The start tick in milliseconds
*/
fun SetStartTick(tick: Long?){
starttick = tick
}
/**
* Get the elapsed time since the start tick
* @return Elapsed time as a formatted string or "N/A" if start tick is not set
*/
fun GetElapsed() : String {
if (starttick != null){
val elapsedMs = System.currentTimeMillis() - starttick!!
val totalSeconds = elapsedMs / 1000
val hours = totalSeconds / 3600
val minutes = (totalSeconds % 3600) / 60
val seconds = totalSeconds % 60
return when {
hours > 0 -> String.format("%02d h:%02d m:%02d s", hours, minutes, seconds)
minutes > 0 -> String.format("%02d m:%02d s", minutes, seconds)
else -> String.format("00:%02d s", seconds)
}
} else {
return ""
}
}
/**
* Add a broadcast zone that uses this Barix device
* @param zoneName The name of the broadcast zone
*/
fun AddUsedByBroadcastZone(zoneName: String){
_usedbybroadcastzone.add(zoneName)
println("Added used by broadcast zone: $zoneName to Barix device $ipaddress")
}
/**
* Add multiple broadcast zones that use this Barix device
* @param zoneNames The list of broadcast zone names
*/
fun AddUsedByBroadcastZone(zoneNames: List<String>){
for (zoneName in zoneNames){
AddUsedByBroadcastZone(zoneName)
}
}
/**
* Clear all broadcast zones that use this Barix device
*/
fun ClearUsedByBroadcastZones(){
_usedbybroadcastzone.clear()
}
/**
* Get a comma-separated string of broadcast zones that use this Barix device
* @return Comma-separated string of broadcast zones
*/
fun GetUsedByBroadcastZones() : String{
return _usedbybroadcastzone.joinToString(", ")
}
/**
* Barix mode flag
@@ -53,6 +134,7 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
mp3Consumer.remove(key)
}
@Suppress("unused")
fun Exists_Mp3_Consumer(key: String) : Boolean{
return mp3Consumer.containsKey(key)
}
@@ -129,14 +211,59 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
return statusData == 1
}
/**
* Check if buffer has enough space (more than 10,000 bytes)
* @return true if buffer has enough space
*/
fun bufferEnough(): Boolean{
return isOnline() && (bufferRemain > 10000)
}
/**
* Send data to Barix device via UDP
* @param data The data to send
* @param cbOK Callback function if sending is successful
* @param cbFail Callback function if sending fails
* @param cbPlaying Callback function to indicate if device is playing
*/
fun SendData(data: ByteArray, cbOK: Consumer<String>, cbFail: Consumer<String>) {
fun SendData(data: ByteArray, cbOK: Consumer<String>, cbFail: Consumer<String>, cbPlaying: Consumer<Boolean>) {
if (data.isNotEmpty()) {
CoroutineScope(Dispatchers.IO).launch {
val bb = ByteBuffer.wrap(data)
while(!bufferEnough()){
delay(20)
Logger.info{"Waiting for StreamerOutput $ipaddress buffer to have enough space: $bufferRemain bytes available, need more than 10000 bytes"}
}
val bufmax = bufferRemain
val bufkosong = 0.2 * bufmax
val bufpenuh = 0.8 * bufmax
Logger.info{"Starting to send data to StreamerOutput $ipaddress on channel $channel, total data size: ${data.size} bytes, bufferRemain: $bufferRemain bytes, bufkosong: $bufkosong bytes, bufpenuh: $bufpenuh bytes"}
// Ide 07/02/2026, kasih buffer dummy 10x1000 byte pertama biar barix kebacanya stabil
for (i in 1..10){
val chunk = ByteArray(1000){0}
udp.send(DatagramPacket(chunk, chunk.size, inet))
delay(5)
println("Sending dummy buffer $i to $ipaddress")
}
// delay interval awal = 5 ms untuk streamer output dan 10 ms untuk barix
// slow down interval = 5 ms untuk streamer output dan 10 ms untuk barix
// speed up interval = 2 ms untuk streamer output dan 5 ms untuk barix
val slowdowninterval = if (BarixMode) 8L else 5L
val speedupinterval = if (BarixMode) 5L else 2L
var delayinterval = if (BarixMode) 8L else 5L
// buat hitung elapsed
CoroutineScope(Dispatchers.IO).launch {
SetStartTick(System.currentTimeMillis())
cbPlaying.accept(true)
do{
delay(1000)
} while (isPlaying())
cbPlaying.accept(false)
SetStartTick(null)
}
while(bb.hasRemaining()){
try {
@@ -144,51 +271,40 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
bb.get(chunk)
while(bufferRemain<chunk.size){
delay(10)
// gas-rem pengiriman
when{
bufferRemain <= bufkosong -> {
if (delayinterval!=slowdowninterval){
delayinterval = slowdowninterval
Logger.info{"Sending to $ipaddress on channel $channel, bufferRemain low: $bufferRemain bytes, slowing down to $delayinterval ms"}
}
}
bufferRemain >= bufpenuh -> {
if (delayinterval!=speedupinterval){
delayinterval = speedupinterval
Logger.info{"Sending to $ipaddress on channel $channel, bufferRemain high: $bufferRemain bytes, speeding up to $delayinterval ms"}
}
}
}
}
udp.send(DatagramPacket(chunk, chunk.size, inet))
mp3encoder.PushData(chunk)
if (_barixmode) delay(10) else delay(1)
delay(delayinterval)
} catch (e: Exception) {
Logger.error{"SendData to $ipaddress failed, message: ${e.message}"}
cbFail.accept("SendData to $ipaddress failed, message: ${e.message}")
cbPlaying.accept(false)
return@launch
}
}
cbOK.accept("SendData to $channel ($ipaddress) succeeded, ${data.size} bytes sent")
Logger.info{"SendData to $channel ($ipaddress) ended, ${data.size} bytes sent"}
cbOK.accept("SendData to $channel ($ipaddress) ended, ${data.size} bytes sent")
}
} else cbFail.accept("SendData to $ipaddress failed, data is empty")
}
/**
* Convert BarixConnection to JsonNode
* @return JsonNode representation of BarixConnection
*/
fun toJsonNode(): JsonNode {
// make json node from index, channel, ipaddress, port, bufferRemain, statusData, vu
return Somecodes.objectmapper.createObjectNode().apply {
put("index", index.toInt())
put("channel", channel)
put("ipaddress", ipaddress)
put("port", port)
put("bufferRemain", bufferRemain)
put("statusData", statusData)
put("vu", vu)
put("isOnline", isOnline())
}
}
/**
* Convert BarixConnection to JSON string
* @return JSON string representation of BarixConnection
*/
fun toJsonString(): String {
return Somecodes.toJsonString(toJsonNode())
}
fun ActivateRelay(relays: List<Int>){
if (relays.isNotEmpty()){
var value : Byte = 0
@@ -201,22 +317,6 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
}
}
/**
* Activate relay on Barix device
* @param relays The relay numbers to activate (1-8)
*/
fun ActivateRelay(vararg relays: Int){
if (relays.isNotEmpty()){
var value : Byte = 0
for (r in relays){
if (r in 1..8){
value = value or (1 shl (r - 1)).toByte()
}
}
SendSimpleCommand(byteArrayOf(0x1A, value, 0x61))
}
}
/**
* Deactivate relay on Barix device
*/
@@ -247,41 +347,7 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
}
}
/**
* Send command to Barix device
* @param command The command to send
* @return true if successful
*/
fun SendCommand(command: String): Boolean {
try {
if (_tcp!=null){
if (!_tcp!!.isClosed){
val bb = command.toByteArray()
val size = bb.size + 4
val b4 = byteArrayOf(
(size shr 24 and 0xFF).toByte(),
(size shr 16 and 0xFF).toByte(),
(size shr 8 and 0xFF).toByte(),
(size and 0xFF).toByte()
)
val out = _tcp!!.getOutputStream()
out.write(b4)
out.write(bb)
out.flush()
Logger.info { "SendCommand to $ipaddress : $command" }
return true
}else {
Logger.error { "Socket to $ipaddress is not connected" }
}
} else {
Logger.error { "Socket to $ipaddress is null" }
}
} catch (e: Exception) {
Logger.error { "Failed to SendCommand to $ipaddress, Message : ${e.message}" }
}
return false
}
override fun close() {
try{

View File

@@ -7,17 +7,15 @@ import org.tinylog.Logger
import java.io.DataInputStream
import java.net.ServerSocket
import java.net.Socket
import java.nio.ByteBuffer
import java.util.function.Consumer
@Suppress("unused")
class TCP_Barix_Command_Server {
lateinit var tcpserver: ServerSocket
lateinit var job: Job
private val socketMap = mutableMapOf<String, Socket>()
private val regex = """STATUSBARIX;(\d+);(\d+)(;(\d+))?"""
private val pattern = Regex(regex)
//private val regex = """STATUSBARIX;(\d+);(\d+)(;(\d+))?"""
//private val pattern = Regex(regex)
/**
* Start TCP Command Server
@@ -43,6 +41,7 @@ class TCP_Barix_Command_Server {
try{
val din = DataInputStream(socket.getInputStream())
var VuZeroCounter = 0L
while (isActive) {
val bb = ByteArray(128)
@@ -60,25 +59,56 @@ class TCP_Barix_Command_Server {
Logger.error { "Error reading length from Streamer Output with IP $key, Message : ${ex.message}" }
continue
}
val str = String(bb,4, stringlength).trim()
var str = String(bb,4, stringlength).trim()
if (str.isBlank()) continue
if (!str.startsWith("STATUSBARIX")) continue
if (str.endsWith("@")) str = str.removeSuffix("@")
if (ValidString(str)) {
// Valid command from Barix is in format $"STATUSBARIX;VU;BuffRemain;StatusData"$
pattern.find(str)?.let { matchResult ->
val (vu, buffremain, statusdata) = matchResult.destructured
val status = BarixStatus(
socket.inetAddress.hostAddress,
vu.toInt(),
buffremain.toInt(),
statusdata.toIntOrNull() ?: 0,
statusdata.isNullOrEmpty() // barix tidak ada statusdata , Q-AG1 ada
)
//Logger.info { "Received valid command from $key : $status" }
cb.accept(status)
} ?: run {
Logger.warn { "Invalid command format from $key : $str" }
// Valid command from StreamerOutput is in format $"STATUSBARIX;VU;BuffRemain;StatusData"$
// Valid command from Barix is in format $"STATUSBARIX;VU;BuffRemain"$
val values = str.split(";")
if (values.size<3) continue
if ("STATUSBARIX" != values[0]) continue
val vu = values[1].toIntOrNull() ?: continue
val buffremain = values[2].toIntOrNull() ?: continue
var status: BarixStatus
when(values.size){
3 ->{
// mode barix
// kadang vu stuck tidak di 0 saat idle,
// jadi kalau vu <512 selama 10 kali berturut2
// dan buffer lebih dari 16000, anggap idle
if ((vu < 512) && (buffremain>=16000)){
VuZeroCounter++
} else {
VuZeroCounter = 0
}
// statusdata = isplaying = , if VuZeroCounter >=10 then idle (0) else playing (1)
val statusdata = if (VuZeroCounter>=10) 0 else 1
status = BarixStatus(
socket.inetAddress.hostAddress,
vu,
buffremain,
statusdata,
true
)
}
4 ->{
// mode Q-AG1
val statusdata = values[3].toIntOrNull() ?: 0
status = BarixStatus(
socket.inetAddress.hostAddress,
vu,
buffremain,
statusdata,
false
)
}
else -> continue
}
cb.accept(status)
}
}

View File

@@ -109,6 +109,14 @@ class Somecodes {
} else cb.accept(-1,-1)
}
/**
* Convert a string of numbers separated by commas or semicolons into a list of integers.
*/
fun StringToListInt(value : String?) : List<Int>{
if (value.isNullOrBlank()) return listOf()
return value.split(",",";").mapNotNull { it.trim().toIntOrNull() }
}
/**
* Clear all files in PagingResult directory.
* If PagingResult directory does not exist, callback with -1,-1.

View File

@@ -72,14 +72,13 @@ class TCP_Android_Command_Server {
//println("Received command from $key : $str")
str.split("@").map { it.trim() }.filter { ValidString(it) }
.forEach {
Logger.info{"Receive command from $key : $it"}
process_command(key,it) { reply ->
try {
val cc = String_to_Byte_Android(reply)
if (cc.isNotEmpty()){
dout.write(cc)
dout.flush()
Logger.info{"Sent reply ${cc.size} bytes to $key : $reply"}
//Logger.info{"Sent reply ${cc.size} bytes to $key : $reply"}
} else Logger.error { "Empty reply to send to $key" }
} catch (e: Exception) {
@@ -149,7 +148,7 @@ class TCP_Android_Command_Server {
* @param cb Callback to send reply string
*/
private fun process_command(key: String, cmd: String, cb: Consumer<String>) {
Logger.info { "Command from $key : $cmd" }
if ("PING" != cmd) Logger.info { "Command from $key : $cmd" }
val parts = cmd.split(";").map { it.trim() }.filter { it.isNotBlank() }
when (parts[0]) {
"GETLOGIN" -> {

View File

@@ -25,4 +25,28 @@ data class BroadcastZones(var index: UInt, var description: String, var SoundCha
override fun toString(): String {
return "BroadcastZones(index=$index, description='$description', SoundChannel='$SoundChannel', id='$id', bp='$bp')"
}
companion object{
/**
* Get a list of relay numbers from the broadcast zone's bp field.
* Currently, supports relays 1 to 8.
* @param bz The BroadcastZones object
* @return List of relay numbers (Int) extracted from the bp field
*/
fun getRelaysFromBroadcastZone(bz : BroadcastZones) : List<Int>{
val result = ArrayList<Int>()
// delimiters either comma or semicolon
val parts = bz.bp.split(",", ";")
for (part in parts){
val relay = part.trim().toIntOrNull()
if (relay != null){
if (relay in 1..8){
result.add(relay)
}
}
}
return result
}
}
}

View File

@@ -1,9 +1,9 @@
package web
class StreamerOutputData(val index: UInt, val channel: String, val ipaddress: String, val vu: Int, val bufferRemain: Int, var isPlaying: Boolean) {
data class StreamerOutputData(val index: UInt, val channel: String, val ipaddress: String, val vu: Int, val bufferRemain: Int, val isPlaying: Boolean, val filename: String, val duration: String, val elapsed: String, val broadcastzones: String) {
companion object{
fun fromBarixConnection(bc: barix.BarixConnection): StreamerOutputData {
return StreamerOutputData(bc.index, bc.channel, bc.ipaddress, bc.vu, bc.bufferRemain, bc.isPlaying())
return StreamerOutputData(bc.index, bc.channel, bc.ipaddress, bc.vu, bc.bufferRemain, bc.isPlaying(), bc.GetAudioFileInfo()?.fileName ?: "", bc.GetAudioFileInfo()?.DurationToString() ?: "", bc.GetElapsed(), bc.GetUsedByBroadcastZones())
}
}
}