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; margin-right: .5rem!important;
} }
.mb-2 {
margin-bottom: .5rem!important;
}
.mb-3 { .mb-3 {
margin-bottom: 1rem!important; 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} ip - The jQuery result should be <h6> element.
* @property {JQuery<HTMLElement> | null} buffer - 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} 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. * @property {JQuery<HTMLElement> | null} vu - The jQuery result should be <progress-bar> element.
*/ */
function getCardByIndex(index) { function getCardByIndex(index) {
let cardname = "ch" + index.toString().padStart(2, '0');
let obj = { let obj = {
// title is <h4> element wiht id `streamertitle${index}`, with index as two digit number, e.g. 01, 02, 03 // 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 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 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 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 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; return obj;
} }
@@ -64,29 +80,71 @@ function UpdateStreamerCard(values) {
values = []; values = [];
} }
let visiblilitychanged = false;
for (let i = 1; i <= 64; i++) { for (let i = 1; i <= 64; i++) {
let vv = values.find(v => v.index === i); const vv = values.find(v => v.index === i);
let card = getCardByIndex(i); const cardname = "ch" + i.toString().padStart(2, '0');
const $card = $(`#${cardname}`);
if (vv) { if (vv) {
// there is value for this index // ada data untuk index i
if (card.title) card.title.text(vv.channel ? vv.channel : `Channel ${i.toString().padStart(2, '0')}`); if ($card.length > 0) {
if (card.ip) card.ip.text(`IP Address: ${vv.ipaddress ? vv.ipaddress : 'N/A'}`); // ada card untuk index i, show card
if (card.buffer) card.buffer.text(`Buffer: ${vv.bufferRemain !== undefined && vv.bufferRemain !== null ? vv.bufferRemain.toString() : 'N/A'}`); if ($card.hasClass('d-none')) {
if (card.status) card.status.text(`Status: ${vv.isPlaying ? 'Playing' : 'Idle'}`); visiblilitychanged = true;
if (card.vu) { $card.removeClass('d-none');
setProgress(i, card.vu, vv.vu, 100); $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 { } else {
// no value for this index, disable the card // tidak ada data untuk index i, hide card
if (card.title) card.title.text(`Channel ${i.toString().padStart(2, '0')}`); if ($card.length > 0) {
if (card.ip) card.ip.text(`IP Address: N/A`); // ada card untuk index i, hide card
if (card.buffer) card.buffer.text(`Buffer: N/A`); if (!$card.hasClass('d-none')) {
if (card.status) card.status.text(`Status: Disconnected`); visiblilitychanged = true;
if (card.vu) { $card.addClass('d-none');
setProgress(i, card.vu, 0, 100); $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');
}
});
}
} }
/** /**
@@ -431,48 +489,48 @@ $(document).ready(function () {
window.addEventListener('ws_connected', () =>{ window.addEventListener('ws_connected', () => {
console.log("overview.js ws_connected event triggered"); 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"); console.log("overview.js ws_disconnected event triggered");
if (intervaljob1) clearInterval(intervaljob1); if (intervaljob1) clearInterval(intervaljob1);
if (intervaljob2) clearInterval(intervaljob2); if (intervaljob2) clearInterval(intervaljob2);
intervaljob1 = null; intervaljob1 = null;
intervaljob2 = null; intervaljob2 = null;
}); });
window.addEventListener('ws_message', ()=>{ window.addEventListener('ws_message', () => {
let rep = event.detail; let rep = event.detail;
let cmd = rep.reply; let cmd = rep.reply;
let data = rep.data; let data = rep.data;
if (cmd && cmd.length > 0) { if (cmd && cmd.length > 0) {
switch (cmd) { switch (cmd) {
case "getPagingQueue": case "getPagingQueue":
let pq = JSON.parse(data); let pq = JSON.parse(data);
window.PagingQueue = []; window.PagingQueue = [];
if (Array.isArray(pq) && pq.length > 0) { if (Array.isArray(pq) && pq.length > 0) {
window.PagingQueue.push(...pq); window.PagingQueue.push(...pq);
} }
fill_pagingqueuetablebody(window.PagingQueue); fill_pagingqueuetablebody(window.PagingQueue);
break; break;
case "getAASQueue": case "getAASQueue":
let aq = JSON.parse(data); let aq = JSON.parse(data);
window.QueueTable = []; window.QueueTable = [];
if (Array.isArray(aq) && aq.length > 0) { if (Array.isArray(aq) && aq.length > 0) {
window.QueueTable.push(...aq); window.QueueTable.push(...aq);
} }
fill_automaticqueuetablebody(window.QueueTable); fill_automaticqueuetablebody(window.QueueTable);
break; break;
case "getStreamerOutputs": case "getStreamerOutputs":
/** /**
* @type {StreamerOutputData[]} * @type {StreamerOutputData[]}
*/ */
let so = JSON.parse(data); let so = JSON.parse(data);
UpdateStreamerCard(so); UpdateStreamerCard(so);
break; break;
}
} }
}
}); });
$(window).on('beforeunload', function () { $(window).on('beforeunload', function () {

File diff suppressed because it is too large Load Diff

View File

@@ -20,20 +20,72 @@
</head> </head>
<body> <body>
<div class="card" id="streamercard"> <div class="card streamercard" id="streamercard">
<div class="card-body card-channel"> <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="row">
<div class="col-12 col-sm-12 col-md-12 col-lg-6 col-xl-6 col-xxl-6"> <div class="col-3">
<h6 class="text-muted mb-2" id="streamerip">IP :&nbsp;192.168.10.10</h6> <p class="w-100 h-100 align-content-center">IP</p>
</div> </div>
<div class="col-12 col-sm-12 col-md-12 col-lg-6 col-xl-6 col-xxl-6"> <div class="col">
<h6 class="text-muted mb-2" id="streamerbuffer">Free : 64KB</h6> <p class="w-100 h-100 align-content-center streamerip" id="streamerip">N/A</p>
</div> </div>
</div> </div>
<p class="card-text" id="streamerstatus">Status : Idle</p> <div class="row">
<div class="progress" id="streamervu"> <div class="col-3">
<div class="progress-bar" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" style="width: 50%;">50%</div> <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> </div>
</div> </div>

View File

@@ -177,13 +177,29 @@ fun main(args: Array<String>) {
while (isActive) { while (isActive) {
delay(1000) delay(1000)
// prioritas 1 , habisin queue paging // 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 // 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 // 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 // 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 { fun isValid() : Boolean {
return fileName.isNotBlank() && fileSize > 0 && duration > 0.0 && bytes.isNotEmpty() 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 package barix
import audio.AudioFileInfo
import audio.Mp3Encoder import audio.Mp3Encoder
import codes.Somecodes
import com.fasterxml.jackson.databind.JsonNode
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -16,7 +15,6 @@ import java.nio.ByteBuffer
import java.util.function.Consumer import java.util.function.Consumer
import kotlin.experimental.or import kotlin.experimental.or
@Suppress("unused")
class BarixConnection(val index: UInt, var channel: String, val ipaddress: String, val port: Int = 5002) : AutoCloseable { class BarixConnection(val index: UInt, var channel: String, val ipaddress: String, val port: Int = 5002) : AutoCloseable {
private var _bR: Int = 0 private var _bR: Int = 0
private var _sd: 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 mp3Consumer = mutableMapOf<String, Consumer<ByteArray>>()
private val udp = DatagramSocket() private val udp = DatagramSocket()
private var _barixmode: Boolean = false 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 * Barix mode flag
@@ -53,6 +134,7 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
mp3Consumer.remove(key) mp3Consumer.remove(key)
} }
@Suppress("unused")
fun Exists_Mp3_Consumer(key: String) : Boolean{ fun Exists_Mp3_Consumer(key: String) : Boolean{
return mp3Consumer.containsKey(key) return mp3Consumer.containsKey(key)
} }
@@ -129,14 +211,59 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
return statusData == 1 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 * Send data to Barix device via UDP
* @param data The data to send * @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()) { if (data.isNotEmpty()) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val bb = ByteBuffer.wrap(data) 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()){ while(bb.hasRemaining()){
try { try {
@@ -144,51 +271,40 @@ class BarixConnection(val index: UInt, var channel: String, val ipaddress: Strin
bb.get(chunk) bb.get(chunk)
while(bufferRemain<chunk.size){ while(bufferRemain<chunk.size){
delay(10) 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)) udp.send(DatagramPacket(chunk, chunk.size, inet))
mp3encoder.PushData(chunk) mp3encoder.PushData(chunk)
if (_barixmode) delay(10) else delay(1) delay(delayinterval)
} catch (e: Exception) { } catch (e: Exception) {
Logger.error{"SendData to $ipaddress failed, message: ${e.message}"}
cbFail.accept("SendData to $ipaddress failed, message: ${e.message}") cbFail.accept("SendData to $ipaddress failed, message: ${e.message}")
cbPlaying.accept(false)
return@launch 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") } 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>){ fun ActivateRelay(relays: List<Int>){
if (relays.isNotEmpty()){ if (relays.isNotEmpty()){
var value : Byte = 0 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 * 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() { override fun close() {
try{ try{

View File

@@ -7,17 +7,15 @@ import org.tinylog.Logger
import java.io.DataInputStream import java.io.DataInputStream
import java.net.ServerSocket import java.net.ServerSocket
import java.net.Socket import java.net.Socket
import java.nio.ByteBuffer
import java.util.function.Consumer import java.util.function.Consumer
@Suppress("unused")
class TCP_Barix_Command_Server { class TCP_Barix_Command_Server {
lateinit var tcpserver: ServerSocket lateinit var tcpserver: ServerSocket
lateinit var job: Job lateinit var job: Job
private val socketMap = mutableMapOf<String, Socket>() private val socketMap = mutableMapOf<String, Socket>()
private val regex = """STATUSBARIX;(\d+);(\d+)(;(\d+))?""" //private val regex = """STATUSBARIX;(\d+);(\d+)(;(\d+))?"""
private val pattern = Regex(regex) //private val pattern = Regex(regex)
/** /**
* Start TCP Command Server * Start TCP Command Server
@@ -43,6 +41,7 @@ class TCP_Barix_Command_Server {
try{ try{
val din = DataInputStream(socket.getInputStream()) val din = DataInputStream(socket.getInputStream())
var VuZeroCounter = 0L
while (isActive) { while (isActive) {
val bb = ByteArray(128) 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}" } Logger.error { "Error reading length from Streamer Output with IP $key, Message : ${ex.message}" }
continue continue
} }
val str = String(bb,4, stringlength).trim() var str = String(bb,4, stringlength).trim()
if (str.isBlank()) continue if (str.isBlank()) continue
if (!str.startsWith("STATUSBARIX")) continue if (!str.startsWith("STATUSBARIX")) continue
if (str.endsWith("@")) str = str.removeSuffix("@")
if (ValidString(str)) { if (ValidString(str)) {
// Valid command from Barix is in format $"STATUSBARIX;VU;BuffRemain;StatusData"$ // Valid command from StreamerOutput is in format $"STATUSBARIX;VU;BuffRemain;StatusData"$
pattern.find(str)?.let { matchResult -> // Valid command from Barix is in format $"STATUSBARIX;VU;BuffRemain"$
val (vu, buffremain, statusdata) = matchResult.destructured val values = str.split(";")
val status = BarixStatus( if (values.size<3) continue
socket.inetAddress.hostAddress, if ("STATUSBARIX" != values[0]) continue
vu.toInt(), val vu = values[1].toIntOrNull() ?: continue
buffremain.toInt(),
statusdata.toIntOrNull() ?: 0, val buffremain = values[2].toIntOrNull() ?: continue
statusdata.isNullOrEmpty() // barix tidak ada statusdata , Q-AG1 ada var status: BarixStatus
) when(values.size){
//Logger.info { "Received valid command from $key : $status" } 3 ->{
cb.accept(status) // mode barix
} ?: run { // kadang vu stuck tidak di 0 saat idle,
Logger.warn { "Invalid command format from $key : $str" } // 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) } 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. * Clear all files in PagingResult directory.
* If PagingResult directory does not exist, callback with -1,-1. * 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") //println("Received command from $key : $str")
str.split("@").map { it.trim() }.filter { ValidString(it) } str.split("@").map { it.trim() }.filter { ValidString(it) }
.forEach { .forEach {
Logger.info{"Receive command from $key : $it"}
process_command(key,it) { reply -> process_command(key,it) { reply ->
try { try {
val cc = String_to_Byte_Android(reply) val cc = String_to_Byte_Android(reply)
if (cc.isNotEmpty()){ if (cc.isNotEmpty()){
dout.write(cc) dout.write(cc)
dout.flush() 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" } } else Logger.error { "Empty reply to send to $key" }
} catch (e: Exception) { } catch (e: Exception) {
@@ -149,7 +148,7 @@ class TCP_Android_Command_Server {
* @param cb Callback to send reply 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" } if ("PING" != cmd) Logger.info { "Command from $key : $cmd" }
val parts = cmd.split(";").map { it.trim() }.filter { it.isNotBlank() } val parts = cmd.split(";").map { it.trim() }.filter { it.isNotBlank() }
when (parts[0]) { when (parts[0]) {
"GETLOGIN" -> { "GETLOGIN" -> {

View File

@@ -25,4 +25,28 @@ data class BroadcastZones(var index: UInt, var description: String, var SoundCha
override fun toString(): String { override fun toString(): String {
return "BroadcastZones(index=$index, description='$description', SoundChannel='$SoundChannel', id='$id', bp='$bp')" 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 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{ companion object{
fun fromBarixConnection(bc: barix.BarixConnection): StreamerOutputData { 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())
} }
} }
} }