Compare commits

..

2 Commits

Author SHA1 Message Date
eba4f7852e Commit 07/08/2025 2025-08-07 15:04:53 +07:00
446f031535 Commit 06/08/2025 2025-08-06 16:35:20 +07:00
17 changed files with 659 additions and 385 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
### IntelliJ IDEA ### ### IntelliJ IDEA ###
out/ out/
audiofile/
!**/src/main/**/out/ !**/src/main/**/out/
!**/src/test/**/out/ !**/src/test/**/out/

View File

@@ -9,6 +9,7 @@
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" /> <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/testResources" type="java-test-resource" /> <sourceFolder url="file://$MODULE_DIR$/testResources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/html" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/html" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/audiofile" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />

View File

@@ -1,16 +0,0 @@
{
"ZelloUsername": "gtcdevice01",
"ZelloPassword": "GtcDev2025",
"ZelloChannel": "GtcDev2025",
"ZelloServer": "community",
"ZelloWorkNetworkName": "",
"ZelloEnterpriseServerDomain": "",
"M1": "",
"M2": "",
"M3": "",
"M4": "",
"M5": "",
"M6": "",
"M7": "",
"M8": ""
}

View File

@@ -1,20 +1,20 @@
$(document).ready(function() { $(document).ready(function() {
// Your code here // Your code here
console.log('pocreceiver.js is ready!'); //console.log('pocreceiver.js is ready!');
const path = window.location.pathname; const path = window.location.pathname;
const ws = new WebSocket('ws://' + window.location.host + path + '/ws'); const ws = new WebSocket('ws://' + window.location.host + path + '/ws');
ws.onopen = function() { ws.onopen = function() {
console.log('WebSocket connection opened'); //console.log('WebSocket connection opened');
$('#indicatorDisconnected').addClass('visually-hidden'); $('#indicatorDisconnected').addClass('visually-hidden');
$('#indicatorConnected').removeClass('visually-hidden'); $('#indicatorConnected').removeClass('visually-hidden');
setInterval(function() { setInterval(function() {
sendCommand({ command: "getZelloStatus" }); sendCommand({ command: "getZelloStatus" });
}, 5000); }, 1000);
}; };
ws.onmessage = function(event) { ws.onmessage = function(event) {
console.log('WebSocket message received:', event.data); //console.log('WebSocket message received:', event.data);
let msg = {}; let msg = {};
try { try {
msg = JSON.parse(event.data); msg = JSON.parse(event.data);
@@ -25,13 +25,13 @@ $(document).ready(function() {
} }
if (msg.reply === "getZelloStatus" && msg.data !== undefined && msg.data.length > 0) { if (msg.reply === "getZelloStatus" && msg.data !== undefined && msg.data.length > 0) {
const zelloData = msg.data; const zelloData = msg.data;
console.log('Zello Status Data:', zelloData); //console.log('Zello Status Data:', zelloData);
$('#zelloStatus').text(zelloData.status); $('#zelloStatus').text(zelloData);
} }
}; };
ws.onclose = function() { ws.onclose = function() {
console.log('WebSocket connection closed'); //console.log('WebSocket connection closed');
$('#indicatorDisconnected').removeClass('visually-hidden'); $('#indicatorDisconnected').removeClass('visually-hidden');
$('#indicatorConnected').addClass('visually-hidden'); $('#indicatorConnected').addClass('visually-hidden');
}; };

View File

@@ -1,46 +1,65 @@
$(document).ready(function() { $(document).ready(function () {
// Your code here // Your code here
console.log('precordedbroadcast.js is ready!'); //console.log('precordedbroadcast.js is ready!');
const path = window.location.pathname; const path = window.location.pathname;
const ws = new WebSocket('ws://' + window.location.host + path + '/ws'); const ws = new WebSocket('ws://' + window.location.host + path + '/ws');
for(let i = 1; i<=8; i++){ for (let i = 1; i <= 8; i++) {
$(`#fileM${i}`).val(''); $(`#fileM${i}`).val('');
$(`#playM${i}`).prop('disabled', true); $(`#playM${i}`).prop('disabled', true);
$(`#stopM${i}`).prop('disabled', true); $(`#stopM${i}`).prop('disabled', true);
} }
ws.onopen = function() { ws.onopen = function () {
console.log('WebSocket connection opened'); //console.log('WebSocket connection opened');
$('#indicatorDisconnected').addClass('visually-hidden'); $('#indicatorDisconnected').addClass('visually-hidden');
$('#indicatorConnected').removeClass('visually-hidden'); $('#indicatorConnected').removeClass('visually-hidden');
sendCommand({ command: "getMessageConfig" }); sendCommand({ command: "getMessageConfig" });
setInterval(function() { setInterval(function () {
sendCommand({ command: "getPlaybackStatus" }); sendCommand({ command: "getPlaybackStatus" });
}, 5000); }, 1000); // every second
}; };
ws.onmessage = function(event) { ws.onmessage = function (event) {
console.log('WebSocket message received:', event.data); //console.log('WebSocket message received:', event.data);
let msg = {}; let msg = {};
try { try {
msg = JSON.parse(event.data); msg = JSON.parse(event.data);
} catch (e) { } catch (e) {
return; return;
} }
if (msg.reply === "getMessageConfig" && msg.data !== undefined) {
const messageConfigdata = msg.data; if (msg.reply && msg.reply.length > 0 && msg.data && msg.data.length > 0) {
console.log('Message Config Data:', messageConfigdata); switch (msg.reply) {
for(let i=1; i<=8; i++){ case "playMessage":
if (msg.data !== "success")
{alert(msg.data);}
else{
$('#playbackStatus').text('Playback started');
}
break;
case "stopMessage":
if (msg.data !== "success")
{alert(msg.data);}
else{
$('#playbackStatus').text('Playback stopped');
}
break;
case "getMessageConfig":
const messageConfigdata = JSON.parse(msg.data);
//console.log('Message Config Data:', messageConfigdata);
for (let i = 1; i <= 8; i++) {
let filetitle = $(`#fileM${i}`); let filetitle = $(`#fileM${i}`);
let playButton = $(`#playM${i}`); let playButton = $(`#playM${i}`);
let stopButton = $(`#stopM${i}`); let stopButton = $(`#stopM${i}`);
let fileInput = messageConfigdata[`M${i}`] || ''; let fileInput = messageConfigdata[`M${i}`];
filetitle.val(fileInput); if (fileInput && fileInput.length > 0) {
if (fileInput.length>0){ filetitle.text(fileInput);
playButton.prop('disabled', false); playButton.prop('disabled', false);
stopButton.prop('disabled', false); stopButton.prop('disabled', false);
playButton.on('click', function() { playButton.removeClass('invisible');
stopButton.removeClass('invisible');
playButton.on('click', function () {
let cmd = { let cmd = {
command: "playMessage", command: "playMessage",
data: `M${i}` data: `M${i}`
@@ -48,7 +67,7 @@ $(document).ready(function() {
sendCommand(cmd); sendCommand(cmd);
}); });
stopButton.on('click', function() { stopButton.on('click', function () {
let cmd = { let cmd = {
command: "stopMessage", command: "stopMessage",
data: `M${i}` data: `M${i}`
@@ -56,24 +75,32 @@ $(document).ready(function() {
sendCommand(cmd); sendCommand(cmd);
}); });
} else { } else {
playButton.prop('disabled', true); filetitle.text('Not configured');
stopButton.prop('disabled', true); playButton.addClass('invisible');
stopButton.addClass('invisible');
}
}
break;
case "getPlaybackStatus":
$('#playbackStatus').text(msg.data);
break;
} }
} }
} else if (msg.reply === "getPlaybackStatus" && msg.data !== undefined && msg.data.length > 0) {
const playbackData = msg.data;
$('#playbackStatus').text(playbackData.status);
}
}; };
ws.onclose = function() { ws.onclose = function () {
console.log('WebSocket connection closed'); //console.log('WebSocket connection closed');
$('#indicatorDisconnected').removeClass('visually-hidden'); $('#indicatorDisconnected').removeClass('visually-hidden');
$('#indicatorConnected').addClass('visually-hidden'); $('#indicatorConnected').addClass('visually-hidden');
}; };
ws.onerror = function(error) { ws.onerror = function (error) {
console.error('WebSocket error:', error); console.error('WebSocket error:', error);
}; };

View File

@@ -1,6 +1,6 @@
$(document).ready(function() { $(document).ready(function () {
// Your initialization code here // Your initialization code here
console.log('setting.js is ready!'); //console.log('setting.js is ready!');
$('#dropdownM1 button').text(''); $('#dropdownM1 button').text('');
$('#dropdownM2 button').text(''); $('#dropdownM2 button').text('');
$('#dropdownM3 button').text(''); $('#dropdownM3 button').text('');
@@ -23,25 +23,27 @@ $(document).ready(function() {
const path = window.location.pathname; const path = window.location.pathname;
const ws = new WebSocket('ws://' + window.location.host + path + '/ws'); const ws = new WebSocket('ws://' + window.location.host + path + '/ws');
ws.onopen = function() { ws.onopen = function () {
console.log('WebSocket connection opened'); //console.log('WebSocket connection opened');
$('#indicatorDisconnected').addClass('visually-hidden'); $('#indicatorDisconnected').addClass('visually-hidden');
$('#indicatorConnected').removeClass('visually-hidden'); $('#indicatorConnected').removeClass('visually-hidden');
sendCommand({ command: "getConfig" }); sendCommand({ command: "getConfig" });
}; };
ws.onmessage = function(event) { ws.onmessage = function (event) {
console.log('WebSocket message received:', event.data); //console.log('WebSocket message received:', event.data);
let msg = {}; let msg = {};
try { try {
msg = JSON.parse(event.data); msg = JSON.parse(event.data);
} catch (e) { } catch (e) {
return; return;
} }
if (msg.reply === "getConfig" && msg.data !== undefined ) { if (msg.reply && msg.reply.length > 0) {
switch (msg.reply) {
case "getConfig":
const configData = JSON.parse(msg.data); const configData = JSON.parse(msg.data);
console.log('Config Data:', configData); //console.log('Config Data:', configData);
$('#zelloUsername').val(configData.zelloUsername || ''); $('#zelloUsername').val(configData.zelloUsername || '');
$('#zelloPassword').val(configData.zelloPassword || ''); $('#zelloPassword').val(configData.zelloPassword || '');
$('#zelloChannel').val(configData.zelloChannel || ''); $('#zelloChannel').val(configData.zelloChannel || '');
@@ -63,10 +65,10 @@ $(document).ready(function() {
const dropdownMenu = $(`#dropdownM${i} .dropdown-menu`); const dropdownMenu = $(`#dropdownM${i} .dropdown-menu`);
const dropdownButton = $(`#dropdownM${i} button`); const dropdownButton = $(`#dropdownM${i} button`);
dropdownMenu.empty(); dropdownMenu.empty();
const messages = configData[`MessageList`] || []; const messages = configData[`messageList`] || [];
messages.forEach((msg, idx) => { messages.forEach((msg, idx) => {
const item = $('<a class="dropdown-item" href="#"></a>').text(msg); const item = $('<a class="dropdown-item" href="#"></a>').text(msg);
item.on('click', function() { item.on('click', function () {
dropdownButton.text(msg); dropdownButton.text(msg);
}); });
dropdownMenu.append(item); dropdownMenu.append(item);
@@ -78,60 +80,83 @@ $(document).ready(function() {
dropdownButton.text(''); dropdownButton.text('');
} }
} }
break;
case "setZelloConfig":
if (msg.data === "success") {
alert('Zello configuration updated successfully.');
} else {
alert('Failed to update Zello configuration: ' + msg.data);
} }
sendCommand({ command: "getConfig" }); // Refresh config after update
break;
case "setMessageConfig":
if (msg.data === "success") {
alert('Message configuration updated successfully.');
} else {
alert('Failed to update Message configuration: ' + msg.data);
}
sendCommand({ command: "getConfig" }); // Refresh config after update
break;
}
}
}; };
ws.onclose = function() { ws.onclose = function () {
console.log('WebSocket connection closed'); //console.log('WebSocket connection closed');
$('#indicatorDisconnected').removeClass('visually-hidden'); $('#indicatorDisconnected').removeClass('visually-hidden');
$('#indicatorConnected').addClass('visually-hidden'); $('#indicatorConnected').addClass('visually-hidden');
}; };
ws.onerror = function(error) { ws.onerror = function (error) {
console.error('WebSocket error:', error); console.error('WebSocket error:', error);
}; };
$('#zellocommunity').on('click', function() { $('#zellocommunity').on('click', function () {
$('#zelloWorkNetworkName').val('').prop('disabled', true); $('#zelloWorkNetworkName').val('').prop('disabled', true);
$('#zelloEnterpriseServerDomain').val('').prop('disabled', true); $('#zelloEnterpriseServerDomain').val('').prop('disabled', true);
}); });
$('#zellowork').on('click', function() { $('#zellowork').on('click', function () {
$('#zelloWorkNetworkName').prop('disabled', false); $('#zelloWorkNetworkName').prop('disabled', false);
$('#zelloEnterpriseServerDomain').val('').prop('disabled', true); $('#zelloEnterpriseServerDomain').val('').prop('disabled', true);
}); });
$('#zelloenterprise').on('click', function() { $('#zelloenterprise').on('click', function () {
$('#zelloWorkNetworkName').val('').prop('disabled', true); $('#zelloWorkNetworkName').val('').prop('disabled', true);
$('#zelloEnterpriseServerDomain').prop('disabled', false); $('#zelloEnterpriseServerDomain').prop('disabled', false);
}); });
$('#btnApplyZello').on('click', function() { $('#btnApplyZello').on('click', function () {
if (ws.readyState === WebSocket.OPEN) { if (ws.readyState === WebSocket.OPEN) {
let xx = {
command: "setZelloConfig"
};
let data = { let data = {
command: "setZelloConfig",
ZelloUsername: $('#zelloUsername').val(), ZelloUsername: $('#zelloUsername').val(),
ZelloPassword: $('#zelloPassword').val(), ZelloPassword: $('#zelloPassword').val(),
ZelloChannel: $('#zelloChannel').val(), ZelloChannel: $('#zelloChannel').val(),
ZelloServer: $('#zellocommunity').is(':checked') ? 'community' : ZelloServer: $('#zellocommunity').is(':checked') ? 'community' :
$('#zellowork').is(':checked') ? 'work' : $('#zellowork').is(':checked') ? 'work' :
$('#zelloenterprise').is(':checked') ? 'enterprise' : '' $('#zelloenterprise').is(':checked') ? 'enterprise' : ''
}; }
if ($('#zellowork').is(':checked')) { if ($('#zellowork').is(':checked')) {
data.ZelloWorkNetworkName = $('#zelloWorkNetworkName').val(); data.ZelloWorkNetworkName = $('#zelloWorkNetworkName').val();
} }
if ($('#zelloenterprise').is(':checked')) { if ($('#zelloenterprise').is(':checked')) {
data.ZelloEnterpriseServerDomain = $('#zelloEnterpriseServerDomain').val(); data.ZelloEnterpriseServerDomain = $('#zelloEnterpriseServerDomain').val();
} }
sendCommand(data); xx.data = JSON.stringify(data);
sendCommand(xx);
} else { } else {
console.warn('WebSocket is not open.'); console.warn('WebSocket is not open.');
} }
}); });
$('#btnApplyMessage').on('click', function() { $('#btnApplyMessage').on('click', function () {
if (ws.readyState === WebSocket.OPEN) { if (ws.readyState === WebSocket.OPEN) {
let data = { let data = {
command: "setMessageConfig", command: "setMessageConfig",
data: JSON.stringify({
M1: $('#dropdownM1 button').text(), M1: $('#dropdownM1 button').text(),
M2: $('#dropdownM2 button').text(), M2: $('#dropdownM2 button').text(),
M3: $('#dropdownM3 button').text(), M3: $('#dropdownM3 button').text(),
@@ -140,6 +165,8 @@ $(document).ready(function() {
M6: $('#dropdownM6 button').text(), M6: $('#dropdownM6 button').text(),
M7: $('#dropdownM7 button').text(), M7: $('#dropdownM7 button').text(),
M8: $('#dropdownM8 button').text() M8: $('#dropdownM8 button').text()
})
}; };
sendCommand(data); sendCommand(data);
} else { } else {
@@ -147,7 +174,7 @@ $(document).ready(function() {
} }
}); });
$('#btnUploadContent').on('click', function() { $('#btnUploadContent').on('click', function () {
const fileInput = document.getElementById('chosenFile'); const fileInput = document.getElementById('chosenFile');
if (!fileInput || !fileInput.files.length) { if (!fileInput || !fileInput.files.length) {
alert('Please select a file to upload.'); alert('Please select a file to upload.');
@@ -164,6 +191,7 @@ $(document).ready(function() {
.then(response => { .then(response => {
if (response.ok) { if (response.ok) {
alert('File uploaded successfully.'); alert('File uploaded successfully.');
sendCommand({ command: "getConfig" }); // Refresh config after upload
} else { } else {
alert('File upload failed.'); alert('File upload failed.');
} }

View File

@@ -35,7 +35,7 @@
<h1 class="text-center">Zello Status</h1> <h1 class="text-center">Zello Status</h1>
</div> </div>
<div class="row"> <div class="row">
<p id="zelloStatus">Paragraph</p> <p class="d-flex justify-content-center" id="zelloStatus">Paragraph</p>
</div> </div>
</div> </div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script> <script src="assets/bootstrap/js/bootstrap.min.js"></script>

View File

@@ -43,7 +43,7 @@
<div class="card"> <div class="card">
<div class="card-body" id="cardM1"> <div class="card-body" id="cardM1">
<h4 class="d-flex justify-content-center card-title">Message1</h4> <h4 class="d-flex justify-content-center card-title">Message1</h4>
<p class="text-start card-text" id="fileM1">File 01</p> <p class="d-flex justify-content-center card-text" id="fileM1">File 01</p>
<div class="row"> <div class="row">
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM1" type="button">Play</button></div> <div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM1" type="button">Play</button></div>
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM1" type="button">Stop</button></div> <div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM1" type="button">Stop</button></div>
@@ -55,7 +55,7 @@
<div class="card"> <div class="card">
<div class="card-body" id="cardM2"> <div class="card-body" id="cardM2">
<h4 class="d-flex justify-content-center card-title">Message2</h4> <h4 class="d-flex justify-content-center card-title">Message2</h4>
<p class="text-start card-text" id="fileM2">File 02</p> <p class="d-flex justify-content-center card-text" id="fileM2">File 02</p>
<div class="row"> <div class="row">
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM2" type="button">Play</button></div> <div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM2" type="button">Play</button></div>
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM2" type="button">Stop</button></div> <div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM2" type="button">Stop</button></div>
@@ -69,7 +69,7 @@
<div class="card"> <div class="card">
<div class="card-body" id="cardM3"> <div class="card-body" id="cardM3">
<h4 class="d-flex justify-content-center card-title">Message3</h4> <h4 class="d-flex justify-content-center card-title">Message3</h4>
<p class="text-start card-text" id="fileM3">File 03</p> <p class="d-flex justify-content-center card-text" id="fileM3">File 03</p>
<div class="row"> <div class="row">
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM3" type="button">Play</button></div> <div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM3" type="button">Play</button></div>
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM3" type="button">Stop</button></div> <div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM3" type="button">Stop</button></div>
@@ -81,7 +81,7 @@
<div class="card"> <div class="card">
<div class="card-body" id="cardM4"> <div class="card-body" id="cardM4">
<h4 class="d-flex justify-content-center card-title">Message4</h4> <h4 class="d-flex justify-content-center card-title">Message4</h4>
<p class="text-start card-text" id="fileM4">File 04</p> <p class="d-flex justify-content-center card-text" id="fileM4">File 04</p>
<div class="row"> <div class="row">
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM4" type="button">Play</button></div> <div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM4" type="button">Play</button></div>
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM4" type="button">Stop</button></div> <div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM4" type="button">Stop</button></div>
@@ -95,7 +95,7 @@
<div class="card"> <div class="card">
<div class="card-body" id="cardM5"> <div class="card-body" id="cardM5">
<h4 class="d-flex justify-content-center card-title">Message5</h4> <h4 class="d-flex justify-content-center card-title">Message5</h4>
<p class="text-start card-text" id="fileM5">File 05</p> <p class="d-flex justify-content-center card-text" id="fileM5">File 05</p>
<div class="row"> <div class="row">
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM5" type="button">Play</button></div> <div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM5" type="button">Play</button></div>
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM5" type="button">Stop</button></div> <div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM5" type="button">Stop</button></div>
@@ -107,7 +107,7 @@
<div class="card"> <div class="card">
<div class="card-body" id="cardM6"> <div class="card-body" id="cardM6">
<h4 class="d-flex justify-content-center card-title">Message6</h4> <h4 class="d-flex justify-content-center card-title">Message6</h4>
<p class="text-start card-text" id="fileM6">File 06</p> <p class="d-flex justify-content-center card-text" id="fileM6">File 06</p>
<div class="row"> <div class="row">
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM6" type="button">Play</button></div> <div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM6" type="button">Play</button></div>
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM6" type="button">Stop</button></div> <div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM6" type="button">Stop</button></div>
@@ -121,7 +121,7 @@
<div class="card"> <div class="card">
<div class="card-body" id="cardM7"> <div class="card-body" id="cardM7">
<h4 class="d-flex justify-content-center card-title">Message7</h4> <h4 class="d-flex justify-content-center card-title">Message7</h4>
<p class="text-start card-text" id="fileM7">File 07</p> <p class="d-flex justify-content-center card-text" id="fileM7">File 07</p>
<div class="row"> <div class="row">
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM7" type="button">Play</button></div> <div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM7" type="button">Play</button></div>
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM7" type="button">Stop</button></div> <div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM7" type="button">Stop</button></div>
@@ -133,7 +133,7 @@
<div class="card"> <div class="card">
<div class="card-body" id="cardM8"> <div class="card-body" id="cardM8">
<h4 class="d-flex justify-content-center card-title">Message8</h4> <h4 class="d-flex justify-content-center card-title">Message8</h4>
<p class="text-start card-text" id="fileM8">File 08</p> <p class="d-flex justify-content-center card-text" id="fileM8">File 08</p>
<div class="row"> <div class="row">
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM8" type="button">Play</button></div> <div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM8" type="button">Play</button></div>
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM8" type="button">Stop</button></div> <div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM8" type="button">Stop</button></div>

View File

@@ -2,7 +2,7 @@ import audio.AudioFilePlayer
import audio.AudioUtility import audio.AudioUtility
import audio.OpusStreamReceiver import audio.OpusStreamReceiver
import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import somecodes.Codes.Companion.ValidString import somecodes.Codes.Companion.ValidString
import somecodes.configFile import somecodes.configFile
import web.WsReply import web.WsReply
@@ -10,8 +10,12 @@ import web.webApp
import zello.ZelloClient import zello.ZelloClient
import zello.ZelloEvent import zello.ZelloEvent
import javafx.util.Pair import javafx.util.Pair
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import somecodes.Codes.Companion.ValidFile import somecodes.Codes
import web.WsCommand import web.WsCommand
import java.util.function.BiFunction import java.util.function.BiFunction
@@ -19,46 +23,205 @@ import java.util.function.BiFunction
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter. // click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
fun main() { fun main() {
val logger = LoggerFactory.getLogger("Main") val logger = LoggerFactory.getLogger("Main")
val objectMapper = ObjectMapper() val objectMapper = jacksonObjectMapper()
val cfg = configFile() val cfg = configFile()
cfg.Load() cfg.Load()
val au = AudioUtility()
var audioID = 0 var audioID = 0
val preferedAudioDevice = "Speakers" val preferedAudioDevice = "Speakers"
au.DetectPlaybackDevices().forEach { pair ->
println("Device ID: ${pair.first}, Name: ${pair.second}") AudioUtility.DetectPlaybackDevices().forEach { pair ->
logger.info("Device ID: ${pair.first}, Name: ${pair.second}")
if (pair.second.contains(preferedAudioDevice)) { if (pair.second.contains(preferedAudioDevice)) {
audioID = pair.first audioID = pair.first
} }
} }
if (audioID!=0){ if (audioID!=0){
val initsuccess = au.InitDevice(audioID) val initsuccess = AudioUtility.InitDevice(audioID,44100)
println("Audio Device $audioID initialized: $initsuccess") logger.info("Audio Device $audioID initialized: $initsuccess")
} }
// for Zello Client
val o = OpusStreamReceiver(audioID) val o = OpusStreamReceiver(audioID)
// for AudioFilePlayer
var afp: AudioFilePlayer? = null var afp: AudioFilePlayer? = null
/**
* Creates a ZelloClient instance based on the configuration.
*/
fun CreateZelloFromConfig(): ZelloClient {
return when (cfg.ZelloServer) {
"work" -> {
ZelloClient.fromZelloWork(
cfg.ZelloUsername ?: "",
cfg.ZelloPassword ?: "",
cfg.ZelloChannel ?: "",
cfg.ZelloWorkNetworkName ?: ""
)
}
"enterprise" -> {
ZelloClient.fromZelloEnterpriseServer(
cfg.ZelloUsername ?: "",
cfg.ZelloPassword ?: "",
cfg.ZelloChannel ?: "",
cfg.ZelloEnterpriseServerDomain ?: ""
)
}
else -> {
ZelloClient.fromConsumerZello(cfg.ZelloUsername ?: "", cfg.ZelloPassword ?: "", cfg.ZelloChannel ?: "")
}
}
}
// Create Zello client from configuration and start it
var z = CreateZelloFromConfig()
// Create ZelloEvent implementation to handle events
val z_event = object : ZelloEvent{
override fun onChannelStatus(
channel: String,
status: String,
userOnline: Int,
error: String?,
errorType: String?
) {
logger.info("Channel Status: $channel is $status with $userOnline users online.")
if (ValidString(error) && ValidString(errorType)) {
logger.info("Error: $error, Type: $errorType")
}
}
override fun onAudioData(streamID: Int, from: String, For: String, channel: String, data: ByteArray) {
logger.info("Audio Data received from $from for $For on channel $channel with streamID $streamID ")
}
override fun onThumbnailImage(imageID: Int, from: String, For: String, channel: String, data: ByteArray, timestamp: Long) {
logger.info("Thumbnail Image received from $from for $For on channel $channel with imageID $imageID at timestamp $timestamp")
}
override fun onFullImage(imageID: Int, from: String, For: String, channel: String, data: ByteArray, timestamp: Long) {
logger.info("Full Image received from $from for $For on channel $channel with imageID $imageID at timestamp $timestamp")
}
override fun onTextMessage(messageID: Int, from: String, For: String, channel: String, text: String, timestamp: Long) {
logger.info("Text Message received from $from for $For on channel $channel with messageID $messageID at timestamp $timestamp. Text: $text")
}
override fun onLocation(messageID: Int, from: String, For: String, channel: String, latitude: Double, longitude: Double, address: String, accuracy: Double, timestamp: Long) {
logger.info("Location received from $from for $For on channel $channel with messageID $messageID at timestamp $timestamp. Location($latitude,$longitude), Address:$address, Accuracy:$accuracy")
}
override fun onConnected() {
logger.info("Connected to Zello server.")
}
override fun onDisconnected(reason: String) {
logger.info("Disconnected from Zello Server, reason: $reason")
logger.info("Reconnecting after 10 seconds...")
z.Stop()
val e = this
CoroutineScope(Dispatchers.Default).launch {
delay(10000) // Wait for 10 seconds before trying to reconnect
z = CreateZelloFromConfig()
z.Start(e)
}
}
override fun onError(errorMessage: String) {
logger.info("Error occurred in Zello client: $errorMessage")
}
override fun onStartStreaming(from: String, For: String, channel: String) {
// stop any previous playback
afp?.Stop()
afp = null
if (o.Start()){
logger.info("Opus Receiver ready for streaming from $from for $For on channel $channel")
} else {
logger.info("Failed to start Opus Receiver for streaming from $from for $For on channel $channel")
}
}
override fun onStopStreaming(from: String, For: String, channel: String) {
o.Stop()
logger.info("Opus Receiver stopped streaming from $from for $For on channel $channel")
}
override fun onStreamingData(
from: String,
For: String,
channel: String,
data: ByteArray
) {
if (o.isPlaying) o.PushData(data)
}
}
z.Start(z_event)
// Start the web application with WebSocket support
val w = webApp("0.0.0.0",3030, BiFunction { val w = webApp("0.0.0.0",3030, BiFunction {
source: String, cmd: WsCommand -> source: String, cmd: WsCommand ->
when (source) { when (source) {
"setting" -> when(cmd.command){ "setting" -> when(cmd.command){
"getConfig" ->{ "getConfig" ->{
logger.info("Get Config") val data = mapOf(
WsReply(cmd.command,objectMapper.writeValueAsString(cfg) ) "zelloUsername" to cfg.ZelloUsername,
"zelloPassword" to cfg.ZelloPassword,
"zelloChannel" to cfg.ZelloChannel,
"zelloServer" to cfg.ZelloServer,
"zelloWorkNetworkName" to cfg.ZelloWorkNetworkName,
"zelloEnterpriseServerDomain" to cfg.ZelloEnterpriseServerDomain,
"m1" to cfg.M1,
"m2" to cfg.M2,
"m3" to cfg.M3,
"m4" to cfg.M4,
"m5" to cfg.M5,
"m6" to cfg.M6,
"m7" to cfg.M7,
"m8" to cfg.M8,
"messageList" to Codes.getAudioFiles()
)
WsReply(cmd.command, objectMapper.writeValueAsString(data).trim())
} }
"setZelloConfig" -> { "setZelloConfig" -> {
try{ try{
val xx = objectMapper.readValue(cmd.data, object: TypeReference<Map<String, String>>() {}) val xx = objectMapper.readValue(cmd.data, object: TypeReference<Map<String, String>>() {})
cfg.ZelloUsername = xx["ZelloUsername"] var changed = false
cfg.ZelloPassword = xx["ZelloPassword"] if (cfg.ZelloUsername != xx["ZelloUsername"]) {
cfg.ZelloChannel = xx["ZelloChannel"] cfg.ZelloUsername = xx["ZelloUsername"] ?: ""
cfg.ZelloServer = xx["ZelloServer"] changed = true
cfg.ZelloWorkNetworkName = xx["ZelloWorkNetworkName"] }
cfg.ZelloEnterpriseServerDomain = xx["ZelloEnterpriseServerDomain"] if (cfg.ZelloPassword != xx["ZelloPassword"]) {
cfg.ZelloPassword = xx["ZelloPassword"] ?: ""
changed = true
}
if (cfg.ZelloChannel != xx["ZelloChannel"]) {
cfg.ZelloChannel = xx["ZelloChannel"] ?: ""
changed = true
}
if (cfg.ZelloServer != xx["ZelloServer"]) {
cfg.ZelloServer = xx["ZelloServer"] ?: ""
changed = true
}
if (cfg.ZelloWorkNetworkName != xx["ZelloWorkNetworkName"]) {
cfg.ZelloWorkNetworkName = xx["ZelloWorkNetworkName"] ?: ""
changed = true
}
if (cfg.ZelloEnterpriseServerDomain != xx["ZelloEnterpriseServerDomain"]) {
cfg.ZelloEnterpriseServerDomain = xx["ZelloEnterpriseServerDomain"] ?: ""
changed = true
}
if (changed){
cfg.Save()
z.Stop()
z = CreateZelloFromConfig()
z.Start(z_event)
WsReply(cmd.command,"success") WsReply(cmd.command,"success")
} else WsReply(cmd.command,"No changes made")
} catch (e: Exception){ } catch (e: Exception){
WsReply(cmd.command,"failed: ${e.message}") WsReply(cmd.command,"failed: ${e.message}")
} }
@@ -68,15 +231,43 @@ fun main() {
try{ try{
val xx = objectMapper.readValue(cmd.data, object : TypeReference<Map<String, String>>() {}) val xx = objectMapper.readValue(cmd.data, object : TypeReference<Map<String, String>>() {})
cfg.M1 = xx["M1"] var changed = false
cfg.M2 = xx["M2"] if (cfg.M1 != xx["M1"]) {
cfg.M3 = xx["M3"] cfg.M1 = xx["M1"] ?: ""
cfg.M4 = xx["M4"] changed = true
cfg.M5 = xx["M5"] }
cfg.M6 = xx["M6"] if (cfg.M2 != xx["M2"]) {
cfg.M7 = xx["M7"] cfg.M2 = xx["M2"] ?: ""
cfg.M8 = xx["M8"] changed = true
}
if (cfg.M3 != xx["M3"]) {
cfg.M3 = xx["M3"] ?: ""
changed = true
}
if (cfg.M4 != xx["M4"]) {
cfg.M4 = xx["M4"] ?: ""
changed = true
}
if (cfg.M5 != xx["M5"]) {
cfg.M5 = xx["M5"] ?: ""
changed = true
}
if (cfg.M6 != xx["M6"]) {
cfg.M6 = xx["M6"] ?: ""
changed = true
}
if (cfg.M7 != xx["M7"]) {
cfg.M7 = xx["M7"] ?: ""
changed = true
}
if (cfg.M8 != xx["M8"]) {
cfg.M8 = xx["M8"] ?: ""
changed = true
}
if (changed){
cfg.Save()
WsReply(cmd.command,"success") WsReply(cmd.command,"success")
} else WsReply(cmd.command,"No changes made")
} catch (e: Exception){ } catch (e: Exception){
WsReply(cmd.command,"failed: ${e.message}") WsReply(cmd.command,"failed: ${e.message}")
} }
@@ -101,12 +292,18 @@ fun main() {
} }
"getPlaybackStatus" ->{ "getPlaybackStatus" ->{
if (afp!=null && true==afp?.isPlaying){ if (afp!=null && true==afp?.isPlaying){
WsReply(cmd.command, "Playing: ${afp?.filename}") WsReply(cmd.command, "Playing: ${afp?.filename}, Duration: ${afp?.duration?.toInt()}, Elapsed: ${afp?.elapsed?.toInt()} seconds")
} else { } else {
WsReply(cmd.command, "Idle") WsReply(cmd.command, "Idle")
} }
} }
"playMessage" ->{ "playMessage" ->{
afp?.Stop()
afp = null
// stop Opus Receiver if it is running
o.Stop()
val filename = when(cmd.data){ val filename = when(cmd.data){
"M1" -> cfg.M1 "M1" -> cfg.M1
"M2" -> cfg.M2 "M2" -> cfg.M2
@@ -120,19 +317,22 @@ fun main() {
null null
} }
} }
if (ValidFile(filename)){ if (filename!=null){
try{ try{
val player= AudioFilePlayer(audioID, filename) afp= AudioFilePlayer(audioID, filename)
player.Play { _ -> afp = null} afp?.Play { _ -> afp = null}
afp = player
WsReply(cmd.command,"success") WsReply(cmd.command,"success")
} catch (e: Exception){ } catch (e: Exception){
afp?.Stop()
afp = null
WsReply(cmd.command, "failed: ${e.message}") WsReply(cmd.command, "failed: ${e.message}")
} }
} else WsReply(cmd.command,"Invalid file : $filename") } else {
afp?.Stop()
afp = null
WsReply(cmd.command,"Invalid message name: ${cmd.data}")
}
} }
"stopMessage" ->{ "stopMessage" ->{
@@ -143,6 +343,16 @@ fun main() {
else -> WsReply(cmd.command,"Invalid command: ${cmd.command}") else -> WsReply(cmd.command,"Invalid command: ${cmd.command}")
} }
"pocreceiver" -> when(cmd.command){ "pocreceiver" -> when(cmd.command){
"getZelloStatus" -> {
var status = "Disconnected"
if (z.currentChannel?.isNotBlank() == true){
status = "Channel: ${z.currentChannel}, Online: ${z.isOnline}, Username: ${z.username}"
if (z.isReceivingStreaming){
status += ", Streaming From: ${z.receivingFrom}, Bytes Received: ${Codes.SizeToString(z.bytesReceived)}"
}
}
WsReply(cmd.command, status)
}
else -> WsReply(cmd.command,"Invalid command: ${cmd.command}") else -> WsReply(cmd.command,"Invalid command: ${cmd.command}")
} }
else -> WsReply(cmd.command,"Invalid source: $source") else -> WsReply(cmd.command,"Invalid source: $source")
@@ -151,76 +361,11 @@ fun main() {
} , Pair("admin","admin1234")) } , Pair("admin","admin1234"))
w.Start() w.Start()
val z = ZelloClient.fromConsumerZello("gtcdevice01","GtcDev2025")
z.Start(object : ZelloEvent {
override fun onChannelStatus(
channel: String,
status: String,
userOnline: Int,
error: String?,
errorType: String?
) {
println("Channel Status: $channel is $status with $userOnline users online.")
if (ValidString(error) && ValidString(errorType)) {
println("Error: $error, Type: $errorType")
}
}
override fun onAudioData(streamID: Int, from: String, For: String, channel: String, data: ByteArray) {
println("Audio Data received from $from for $For on channel $channel with streamID $streamID ")
}
override fun onThumbnailImage(imageID: Int, from: String, For: String, channel: String, data: ByteArray, timestamp: Long) {
println("Thumbnail Image received from $from for $For on channel $channel with imageID $imageID at timestamp $timestamp")
}
override fun onFullImage(imageID: Int, from: String, For: String, channel: String, data: ByteArray, timestamp: Long) {
println("Full Image received from $from for $For on channel $channel with imageID $imageID at timestamp $timestamp")
}
override fun onTextMessage(messageID: Int, from: String, For: String, channel: String, text: String, timestamp: Long) {
println("Text Message received from $from for $For on channel $channel with messageID $messageID at timestamp $timestamp. Text: $text")
}
override fun onLocation(messageID: Int, from: String, For: String, channel: String, latitude: Double, longitude: Double, address: String, accuracy: Double, timestamp: Long) {
println("Location received from $from for $For on channel $channel with messageID $messageID at timestamp $timestamp. Location($latitude,$longitude), Address:$address, Accuracy:$accuracy")
}
override fun onConnected() {
println("Connected to Zello server.")
}
override fun onDisconnected() {
println("Disconnected from Zello server.")
}
override fun onError(errorMessage: String) {
println("Error occurred in Zello client: $errorMessage")
}
override fun onStartStreaming(from: String, For: String, channel: String) {
if (o.Start()){
println("Opus Receiver ready for streaming from $from for $For on channel $channel")
} else {
println("Failed to start Opus Receiver for streaming from $from for $For on channel $channel")
}
}
override fun onStopStreaming(from: String, For: String, channel: String) {
o.Stop()
println("Opus Receiver stopped streaming from $from for $For on channel $channel")
}
override fun onStreamingData(
from: String,
For: String,
channel: String,
data: ByteArray
) {
if (o.isPlaying) o.PushData(data)
}
})
} }

View File

@@ -4,71 +4,67 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import somecodes.Codes
import java.util.function.Consumer import java.util.function.Consumer
import kotlin.io.path.absolutePathString
import kotlin.io.path.exists
/** /**
* Audio Player for playing audio files. * Audio Player for playing audio files.
* Supported extensions : .wav, .mp3 * Supported extensions : .wav, .mp3
*/ */
@Suppress("unused") @Suppress("unused")
class AudioFilePlayer(deviceID: Int, val filename: String?, device_samplingrate: Int = 48000) { class AudioFilePlayer(deviceID: Int, val filename: String, device_samplingrate: Int = 48000) {
val bass: Bass = Bass.Instance private val bass: Bass = Bass.Instance
var filehandle = 0 private var filehandle = 0
var isPlaying = false var isPlaying = false
val fileSize: Long
val duration: Double
var elapsed: Double = 0.0
init{ init{
if (bass.BASS_SetDevice(deviceID)){ val fullpath = Codes.audioFilePath.resolve(filename)
filehandle = bass.BASS_StreamCreateFile(false, filename, 0, 0, 0) if (fullpath.exists()){
if (filehandle == 0) { if (AudioUtility.InitDevice(deviceID, device_samplingrate)) {
throw Exception("Failed to create stream for file $filename: ${bass.BASS_ErrorGetCode()}") filehandle = bass.BASS_StreamCreateFile(false, fullpath.absolutePathString(), 0, 0, 0)
} if (filehandle!=0){
} else throw Exception("Failed to set device $deviceID") fileSize = bass.BASS_ChannelGetLength(filehandle, Bass.BASS_POS_BYTE)
duration = bass.BASS_ChannelBytes2Seconds(filehandle, fileSize)
} else throw Exception("Failed to create stream for file $filename: ${bass.BASS_ErrorGetCode()}")
} else throw Exception("Error initializing device $deviceID with sampling rate $device_samplingrate, code ${bass.BASS_ErrorGetCode()}")
} else throw Exception("File $filename does not exists")
} }
fun Stop(){ fun Stop(){
if (filehandle!=0){ if (filehandle!=0){
bass.BASS_ChannelStop(filehandle) bass.BASS_ChannelFree(filehandle)
bass.BASS_StreamFree(filehandle)
filehandle = 0 filehandle = 0
} }
isPlaying = false
AudioUtility.Free()
} }
fun Play(finished: Consumer<Any> ) : Boolean{ fun Play(finished: Consumer<Any> ) : Boolean{
if (bass.BASS_ChannelPlay(filehandle, false)){ if (bass.BASS_ChannelPlay(filehandle, false)){
elapsed = 0.0
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
isPlaying = true isPlaying = true
while(true){ while(true){
delay(1000) delay(50)
if (bass.BASS_ChannelIsActive(filehandle)!= Bass.BASS_ACTIVE_PLAYING){ if (bass.BASS_ChannelIsActive(filehandle)!= Bass.BASS_ACTIVE_PLAYING){
// finished playing // finished playing
break break
} }
elapsed = bass.BASS_ChannelBytes2Seconds(filehandle, bass.BASS_ChannelGetPosition(filehandle, Bass.BASS_POS_BYTE))
} }
isPlaying = false isPlaying = false
bass.BASS_StreamFree(filehandle) bass.BASS_ChannelFree(filehandle)
filehandle = 0 filehandle = 0
finished.accept(true) finished.accept(true)
} }
// Revisi 06/08/2025 Ganti thread dengan Coroutine
// val thread = Thread{
// isPlaying = true
// while(true){
// Thread.sleep(1000)
//
// if (bass.BASS_ChannelIsActive(filehandle)!= Bass.BASS_ACTIVE_PLAYING){
// // finished playing
// break
// }
// }
// isPlaying = false
// bass.BASS_StreamFree(filehandle)
// filehandle = 0
// finished.accept(true)
// }
// thread.name = "AudioFilePlayer $filename"
// thread.isDaemon = true
// thread.start()
return true return true
} }
return false return false

View File

@@ -3,12 +3,13 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
class AudioUtility { class AudioUtility {
private val bass = Bass.Instance
private val logger : Logger = LoggerFactory.getLogger(AudioUtility::class.java) private val logger : Logger = LoggerFactory.getLogger(AudioUtility::class.java)
init{
logger.info("Bass Version = ${bass.BASS_GetVersion().toHexString()}")
} companion object{
private val bass = Bass.Instance
fun DetectPlaybackDevices() : List<Pair<Int, String>> { fun DetectPlaybackDevices() : List<Pair<Int, String>> {
val result = ArrayList<Pair<Int, String>>() val result = ArrayList<Pair<Int, String>>()
@@ -38,4 +39,25 @@ class AudioUtility {
return false // gagal GetDeviceInfo return false // gagal GetDeviceInfo
} }
/**
* Check if the device is having some handles opened.
* @param deviceID the device ID to check
* @return true if the device is opening something, false otherwise
*/
fun DeviceIsOpeningSomething(deviceID: Int) : Boolean {
return bass.BASS_GetConfig(Bass.BASS_CONFIG_HANDLES) > 0
}
fun Free(){
bass.BASS_Free()
}
}
init{
logger.info("Bass Version = ${bass.BASS_GetVersion().toHexString()}")
}
} }

View File

@@ -12,7 +12,7 @@ import org.slf4j.LoggerFactory
* @throws Exception if the device cannot be set. * @throws Exception if the device cannot be set.
*/ */
@Suppress("unused") @Suppress("unused")
class OpusStreamReceiver(deviceID: Int, val samplingrate: Int = 16000) { class OpusStreamReceiver(val deviceID: Int, val samplingrate: Int = 16000) {
private val bass = Bass.Instance private val bass = Bass.Instance
private val bassopus = BASSOPUS.Instance private val bassopus = BASSOPUS.Instance
private var filehandle = 0 private var filehandle = 0
@@ -20,17 +20,15 @@ class OpusStreamReceiver(deviceID: Int, val samplingrate: Int = 16000) {
var isPlaying = false var isPlaying = false
init{
if (!bass.BASS_SetDevice(deviceID)){
throw Exception("Failed to set device $deviceID")
}
}
/** /**
* Starts the Opus stream playback. * Starts the Opus stream playback.
* @return true if the stream started successfully, false otherwise. * @return true if the stream started successfully, false otherwise.
*/ */
fun Start() : Boolean{ fun Start() : Boolean{
if (AudioUtility.InitDevice(deviceID, samplingrate)){
val opushead = BASSOPUS.BASS_OPUS_HEAD() val opushead = BASSOPUS.BASS_OPUS_HEAD()
opushead.version = 1 opushead.version = 1
opushead.channels = 1 opushead.channels = 1
@@ -43,6 +41,8 @@ class OpusStreamReceiver(deviceID: Int, val samplingrate: Int = 16000) {
return true return true
} else logger.error("BASS_ChannelPlay failed for filehandle $filehandle, code ${bass.BASS_ErrorGetCode()}") } else logger.error("BASS_ChannelPlay failed for filehandle $filehandle, code ${bass.BASS_ErrorGetCode()}")
} else logger.error("BASS_OPUS_StreamCreate failed, code ${bass.BASS_ErrorGetCode()}") } else logger.error("BASS_OPUS_StreamCreate failed, code ${bass.BASS_ErrorGetCode()}")
} else logger.error("Error initializing device $deviceID with sampling rate $samplingrate, code ${bass.BASS_ErrorGetCode()}")
return false return false
} }
@@ -51,11 +51,11 @@ class OpusStreamReceiver(deviceID: Int, val samplingrate: Int = 16000) {
*/ */
fun Stop(){ fun Stop(){
if (filehandle!=0){ if (filehandle!=0){
bass.BASS_ChannelStop(filehandle) bass.BASS_ChannelFree(filehandle)
bass.BASS_StreamFree(filehandle)
filehandle = 0 filehandle = 0
isPlaying = false
} }
isPlaying = false
AudioUtility.Free()
} }
/** /**

View File

@@ -1,12 +1,42 @@
package somecodes package somecodes
import com.fasterxml.jackson.databind.ObjectMapper
import java.io.File import java.io.File
import kotlin.io.path.Path
@Suppress("unused") @Suppress("unused")
class Codes { class Codes {
private val objectMapper = ObjectMapper()
companion object{ companion object{
val audioFilePath = Path(System.getProperty("user.dir"), "audiofile")
private val validAudioExtensions = setOf("wav", "mp3")
private val KB_size = 1024 // 1 KB = 1024 bytes
private val MB_size = 1024 * KB_size // 1 MB = 1024 KB
private val GB_size = 1024 * MB_size // 1 GB = 1024 MB
fun SizeToString(size: Long) : String {
return when {
size >= GB_size -> String.format("%.2f GB", size.toDouble() / GB_size)
size >= MB_size -> String.format("%.2f MB", size.toDouble() / MB_size)
size >= KB_size -> String.format("%.2f KB", size.toDouble() / KB_size)
else -> "$size bytes"
}
}
fun getAudioFiles() : Array<String> {
val audioDir = audioFilePath.toFile()
if (!audioDir.exists()) {
audioDir.mkdirs() // Create directory if it doesn't exist
}
val ll = audioDir.listFiles()?.filter { it.isFile && validAudioExtensions.contains(it.extension) }?.map { it.name } ?: emptyList()
return ll.toTypedArray()
}
fun getaudioFileFullPath(filename: String) : String {
if (ValidString(filename)) {
val file = audioFilePath.resolve(filename)
return file.toAbsolutePath().toString()
}
return ""
}
fun ValidFile(s: String?) : Boolean { fun ValidFile(s: String?) : Boolean {
if (s!=null){ if (s!=null){

View File

@@ -1,5 +1,6 @@
package somecodes package somecodes
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.Path import kotlin.io.path.Path
@@ -21,15 +22,14 @@ class configFile {
private val filepath : Path = Path(System.getProperty("user.dir"), "config.json") private val filepath : Path = Path(System.getProperty("user.dir"), "config.json")
@Suppress("UNCHECKED_CAST")
fun Load(){ fun Load(){
if (filepath.toFile().exists()){ if (filepath.toFile().exists()){
// file found, then load the configuration to configFile object // file found, then load the configuration to configFile object
try{ try{
val json = filepath.toFile().readText() val json = filepath.toFile().readText()
val configMap = json.split(",").associate { it -> val objectMapper = jacksonObjectMapper()
val (key, value) = it.split(":").map { it.trim().removeSurrounding("\"") } val configMap = objectMapper.readValue(json, Map::class.java) as Map<String, String>
key to value
}
ZelloUsername = configMap["ZelloUsername"] ZelloUsername = configMap["ZelloUsername"]
ZelloPassword = configMap["ZelloPassword"] ZelloPassword = configMap["ZelloPassword"]
@@ -72,23 +72,25 @@ class configFile {
fun Save(){ fun Save(){
try { try {
// Convert the configFile object to JSON and write it to the file // Convert the configFile object to JSON and write it to the file
val json = """{ val js = mapOf(
"ZelloUsername": "$ZelloUsername", "ZelloUsername" to ZelloUsername,
"ZelloPassword": "$ZelloPassword", "ZelloPassword" to ZelloPassword,
"ZelloChannel": "$ZelloChannel", "ZelloChannel" to ZelloChannel,
"ZelloServer": "$ZelloServer", "ZelloServer" to ZelloServer,
"ZelloWorkNetworkName": "$ZelloWorkNetworkName", "ZelloWorkNetworkName" to ZelloWorkNetworkName,
"ZelloEnterpriseServerDomain": "$ZelloEnterpriseServerDomain", "ZelloEnterpriseServerDomain" to ZelloEnterpriseServerDomain,
"M1": "$M1", "M1" to M1,
"M2": "$M2", "M2" to M2,
"M3": "$M3", "M3" to M3,
"M4": "$M4", "M4" to M4,
"M5": "$M5", "M5" to M5,
"M6": "$M6", "M6" to M6,
"M7": "$M7", "M7" to M7,
"M8": "$M8" "M8" to M8
}""" )
filepath.toFile().writeText(json)
val mapper = jacksonObjectMapper()
mapper.writerWithDefaultPrettyPrinter().writeValue(filepath.toFile(), js)
} catch (e: Exception) { } catch (e: Exception) {
println("Error saving configuration: ${e.message}") println("Error saving configuration: ${e.message}")
} }

View File

@@ -2,10 +2,15 @@ package web;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.javalin.Javalin; import io.javalin.Javalin;
import io.javalin.http.HttpStatus;
import javafx.util.Pair; import javafx.util.Pair;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import somecodes.Codes;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.function.BiFunction; import java.util.function.BiFunction;
@@ -66,7 +71,7 @@ public class webApp {
String message = wsMessageContext.message(); String message = wsMessageContext.message();
try{ try{
var command = objectMapper.readValue(message, WsCommand.class); var command = objectMapper.readValue(message, WsCommand.class);
logger.info("Received command from pocreceiver.html/ws : {}", command); //logger.info("Received command from pocreceiver.html/ws : {}", command);
var reply = callback.apply("pocreceiver", command); var reply = callback.apply("pocreceiver", command);
wsMessageContext.send(reply); wsMessageContext.send(reply);
} catch (Exception e){ } catch (Exception e){
@@ -85,7 +90,7 @@ public class webApp {
String message = wsMessageContext.message(); String message = wsMessageContext.message();
try{ try{
var command = objectMapper.readValue(message, WsCommand.class); var command = objectMapper.readValue(message, WsCommand.class);
logger.info("Received command from prerecordedbroadcast.html/ws : {}", command); //logger.info("Received command from prerecordedbroadcast.html/ws : {}", command);
var reply = callback.apply("prerecordedbroadcast", command); var reply = callback.apply("prerecordedbroadcast", command);
wsMessageContext.send(reply); wsMessageContext.send(reply);
} catch (Exception e){ } catch (Exception e){
@@ -99,14 +104,33 @@ public class webApp {
ctx.redirect("/index.html"); ctx.redirect("/index.html");
} }
}); });
post("/upload", ctx -> {
// Handle file upload
var file = ctx.uploadedFile("file");
if (file != null ) {
// Process the uploaded file
try(InputStream in = file.content()) {
var savetarget = Codes.Companion.getAudioFilePath().resolve(file.filename());
Files.copy(in, savetarget, StandardCopyOption.REPLACE_EXISTING);
logger.info("File uploaded: {}", file.filename());
ctx.status(HttpStatus.OK).result("File uploaded successfully: " + file.filename());
} catch (Exception e){
ctx.status(HttpStatus.INTERNAL_SERVER_ERROR).result("File upload failed: " + file.filename());
}
} else {
ctx.status(HttpStatus.BAD_REQUEST).result("No file uploaded");
}
});
ws("/ws", wshandler -> wshandler.onMessage(wsMessageContext -> { ws("/ws", wshandler -> wshandler.onMessage(wsMessageContext -> {
// Handle incoming WebSocket messages // Handle incoming WebSocket messages
String message = wsMessageContext.message(); String message = wsMessageContext.message();
//logger.info("Received message from setting.html/ws: {}", message);
try{ try{
var command = objectMapper.readValue(message, WsCommand.class); var command = objectMapper.readValue(message, WsCommand.class);
logger.info("Received command from setting.html/ws : {}", command); //logger.info("Received command from setting.html/ws : {}", command);
var reply = callback.apply("setting", command); var reply = callback.apply("setting", command);
logger.info("Replying to setting.html/ws : {}", reply); //logger.info("Replying to setting.html/ws : {}", reply);
wsMessageContext.send(reply); wsMessageContext.send(reply);
} catch (Exception e){ } catch (Exception e){
logger.error("Error processing {} from setting.html/ws: {}", message, e.getMessage()); logger.error("Error processing {} from setting.html/ws: {}", message, e.getMessage());

View File

@@ -2,10 +2,6 @@ package zello
import com.fasterxml.jackson.module.kotlin.contains import com.fasterxml.jackson.module.kotlin.contains
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.java_websocket.client.WebSocketClient import org.java_websocket.client.WebSocketClient
import org.java_websocket.handshake.ServerHandshake import org.java_websocket.handshake.ServerHandshake
import org.slf4j.Logger import org.slf4j.Logger
@@ -20,23 +16,27 @@ import java.util.function.BiConsumer
/** /**
* ZelloClient is a WebSocket client for connecting to Zello services. * ZelloClient is a WebSocket client for connecting to Zello services.
* [Source](https://github.com/zelloptt/zello-channel-api/blob/master/API.md) * [Source](https://github.com/zelloptt/zello-channel-api/blob/master/API.md)
* @param address the WebSocket address of the Zello server, e.g. "wss://zello.io/ws"
* @param username the username for Zello authentication
* @param password the password for Zello authentication
* @param channel the default channel to join after connecting
* *
*/ */
class ZelloClient(val address : URI, val username: String, val password: String) { class ZelloClient(val address : URI, val username: String, val password: String, val channel: String="GtcDev2025") {
private val streamJob = HashMap<Int, ZelloAudioJob>() private val streamJob = HashMap<Int, ZelloAudioJob>()
private val imageJob = HashMap<Int, ZelloImageJob>() private val imageJob = HashMap<Int, ZelloImageJob>()
private val commandJob = HashMap<Int, Any>() private val commandJob = HashMap<Int, Any>()
companion object { companion object {
fun fromConsumerZello(username : String, password: String) : ZelloClient { fun fromConsumerZello(username : String, password: String, channel: String) : ZelloClient {
return ZelloClient(URI.create("wss://zello.io/ws"), username, password) return ZelloClient(URI.create("wss://zello.io/ws"), username, password, channel)
} }
fun fromZelloWork(username: String, password: String, networkName : String) : ZelloClient{ fun fromZelloWork(username: String, password: String, channel: String, networkName : String) : ZelloClient{
return ZelloClient(URI.create("wss://zellowork.io/ws/$networkName"), username, password) return ZelloClient(URI.create("wss://zellowork.io/ws/$networkName"), username, password, channel)
} }
fun fromZelloEnterpriseServer(username: String, password: String, serverDomain: String) : ZelloClient{ fun fromZelloEnterpriseServer(username: String, password: String, channel: String, serverDomain: String) : ZelloClient{
return ZelloClient(URI.create("wss://$serverDomain/ws/mesh"), username, password) return ZelloClient(URI.create("wss://$serverDomain/ws/mesh"), username, password, channel)
} }
} }
@@ -45,21 +45,29 @@ class ZelloClient(val address : URI, val username: String, val password: String)
// this key is temporary, valid only 30 days from 2025-07-29 // this key is temporary, valid only 30 days from 2025-07-29
// if need to create, from https://developers.zello.com/keys // if need to create, from https://developers.zello.com/keys
private val developerKey : String = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJXa002Y21ScllYSjBiMjV2T2pFLi1yYjJ2THFRbUhYV3dKY2I2azl2TDdUMEtzRWZMRjcxZm5jcktTZ0s2ZE0iLCJleHAiOjE3NTY0MzIyMTIsImF6cCI6ImRldiJ9.ANK7BIS6WVVWsQRjcZXyGWrV2RodCUQD4WXWaA6E4Dlyy8bBCMFdbiKN2D7B_x729HQULailnfRhbXF4Avfg14qONdc1XE_0iGiPUO1kfUSgdd11QylOzjxy6FTKSeZmHOh65JZq2dIWxobCcva-RPvbR8TA656upHh32xrWv9zlU0N707FTca04kze0Iq-q-uC5EL82yK10FEvOPDX88MYy71QRYi8Qh_KbSyMcYAhe2bTsiyjm51ZH9ntkRHd0HNiaijNZI6-qXkkp5Soqmzh-bTtbbgmbX4BT3Qpz_IP3epaX3jl_Aq5DHxXwCsJ9FThif9um5D0TWVGQteR0cQ" private val developerKey : String = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJXa002Y21ScllYSjBiMjV2T2pFLi1yYjJ2THFRbUhYV3dKY2I2azl2TDdUMEtzRWZMRjcxZm5jcktTZ0s2ZE0iLCJleHAiOjE3NTY0MzIyMTIsImF6cCI6ImRldiJ9.ANK7BIS6WVVWsQRjcZXyGWrV2RodCUQD4WXWaA6E4Dlyy8bBCMFdbiKN2D7B_x729HQULailnfRhbXF4Avfg14qONdc1XE_0iGiPUO1kfUSgdd11QylOzjxy6FTKSeZmHOh65JZq2dIWxobCcva-RPvbR8TA656upHh32xrWv9zlU0N707FTca04kze0Iq-q-uC5EL82yK10FEvOPDX88MYy71QRYi8Qh_KbSyMcYAhe2bTsiyjm51ZH9ntkRHd0HNiaijNZI6-qXkkp5Soqmzh-bTtbbgmbX4BT3Qpz_IP3epaX3jl_Aq5DHxXwCsJ9FThif9um5D0TWVGQteR0cQ"
// default channel to join
private var channels = arrayOf("GtcDev2025")
// refresh token for the session // refresh token for the session
// this is set after the first LogonReply // this is set after the first LogonReply
private var refresh_token: String? = null private var refresh_token: String? = null
private val logger : Logger = LoggerFactory.getLogger(ZelloClient::class.java) private val logger : Logger = LoggerFactory.getLogger(ZelloClient::class.java)
private val mapper = jacksonObjectMapper() private val mapper = jacksonObjectMapper()
var currentChannel: String? = null ; private set
var isOnline: Boolean = false ; private set
var isReceivingStreaming: Boolean = false
var receivingFrom: String? = null; private set
var bytesReceived: Long = 0; private set
fun Start(event: ZelloEvent){ fun Start(event: ZelloEvent){
client = object : WebSocketClient(address) { client = object : WebSocketClient(address) {
override fun onOpen(handshakedata: ServerHandshake?) { override fun onOpen(handshakedata: ServerHandshake?) {
//logger.info("Connected to $address") //logger.info("Connected to $address")
isOnline = false
currentChannel = null
isReceivingStreaming = false
seqID = 0 seqID = 0
inc_seqID() inc_seqID()
val lg = LogonCommand.create(seqID,channels, developerKey, username, password) val lg = LogonCommand.create(seqID,arrayOf(channel), developerKey, username, password)
val value = mapper.writeValueAsString(lg) val value = mapper.writeValueAsString(lg)
send(value) send(value)
} }
@@ -82,6 +90,7 @@ class ZelloClient(val address : URI, val username: String, val password: String)
job.pushAudioData(data) job.pushAudioData(data)
event.onStreamingData(job.from, job.For?:"", job.channel, data) event.onStreamingData(job.from, job.For?:"", job.channel, data)
} }
bytesReceived += data.size
} }
0x02.toByte() ->{ 0x02.toByte() ->{
@@ -130,7 +139,7 @@ class ZelloClient(val address : URI, val username: String, val password: String)
refresh_token = lgreply.refresh_token refresh_token = lgreply.refresh_token
event.onConnected() event.onConnected()
} else { } else {
logger.error("Failed to logon: ${lgreply.error ?: "Unknown error"}") event.onError("Failed to logon: ${lgreply.error ?: "Unknown error"}")
} }
} }
else ->{ else ->{
@@ -163,6 +172,8 @@ class ZelloClient(val address : URI, val username: String, val password: String)
"on_channel_status" -> { "on_channel_status" -> {
val channelstatus = mapper.treeToValue(jsnode, Event_OnChannelStatus::class.java) val channelstatus = mapper.treeToValue(jsnode, Event_OnChannelStatus::class.java)
event.onChannelStatus(channelstatus.channel, channelstatus.status, channelstatus.users_online, channelstatus.error, channelstatus.error_type) event.onChannelStatus(channelstatus.channel, channelstatus.status, channelstatus.users_online, channelstatus.error, channelstatus.error_type)
currentChannel = channelstatus.channel
isOnline = channelstatus.status== "online"
} }
"on_error" -> { "on_error" -> {
val error = mapper.treeToValue(jsnode, Event_OnError::class.java) val error = mapper.treeToValue(jsnode, Event_OnError::class.java)
@@ -173,6 +184,9 @@ class ZelloClient(val address : URI, val username: String, val password: String)
logger.info("Stream started on channel ${streamstart.channel} from ${streamstart.from} for ${streamstart.For}") logger.info("Stream started on channel ${streamstart.channel} from ${streamstart.from} for ${streamstart.For}")
streamJob[streamstart.stream_id] = ZelloAudioJob.fromEventOnStreamStart(streamstart) streamJob[streamstart.stream_id] = ZelloAudioJob.fromEventOnStreamStart(streamstart)
event.onStartStreaming(streamstart.from, streamstart.For, streamstart.channel) event.onStartStreaming(streamstart.from, streamstart.For, streamstart.channel)
isReceivingStreaming = true
receivingFrom = streamstart.from
bytesReceived = 0 // reset bytes received
} }
"on_stream_stop" -> { "on_stream_stop" -> {
val streamstop = mapper.treeToValue(jsnode, Event_OnStreamStop::class.java) val streamstop = mapper.treeToValue(jsnode, Event_OnStreamStop::class.java)
@@ -184,6 +198,8 @@ class ZelloClient(val address : URI, val username: String, val password: String)
event.onStopStreaming(job.from, job.For?:"", job.channel) event.onStopStreaming(job.from, job.For?:"", job.channel)
event.onAudioData(job.streamID, job.from, job.For?:"", job.channel, job.getAudioData()) event.onAudioData(job.streamID, job.from, job.For?:"", job.channel, job.getAudioData())
} }
isReceivingStreaming = false
receivingFrom = null
} }
"on_image" ->{ "on_image" ->{
val image = mapper.treeToValue(jsnode, Event_OnImage::class.java) val image = mapper.treeToValue(jsnode, Event_OnImage::class.java)
@@ -209,12 +225,10 @@ class ZelloClient(val address : URI, val username: String, val password: String)
override fun onClose(code: Int, reason: String?, remote: Boolean) { override fun onClose(code: Int, reason: String?, remote: Boolean) {
logger.info("Closed from ${if (remote) "Remote side" else "Local side"}, Code=$code, Reason=$reason") logger.info("Closed from ${if (remote) "Remote side" else "Local side"}, Code=$code, Reason=$reason")
event.onDisconnected() event.onDisconnected(reason?: "Unknown reason")
// try reconnecting after 10 seconds isOnline = false
CoroutineScope(Dispatchers.Default).launch { currentChannel = null
delay(10000)
connect()
}
//Revisi 06/08/2025 : Change to Coroutines //Revisi 06/08/2025 : Change to Coroutines
// val thread = Thread { // val thread = Thread {

View File

@@ -9,7 +9,7 @@ interface ZelloEvent {
fun onTextMessage(messageID: Int, from: String, For: String, channel: String, text: String, timestamp: Long) fun onTextMessage(messageID: Int, from: String, For: String, channel: String, text: String, timestamp: Long)
fun onLocation(messageID: Int, from: String, For: String, channel: String, latitude: Double, longitude: Double, address:String, accuracy: Double, timestamp: Long ) fun onLocation(messageID: Int, from: String, For: String, channel: String, latitude: Double, longitude: Double, address:String, accuracy: Double, timestamp: Long )
fun onConnected() fun onConnected()
fun onDisconnected() fun onDisconnected(reason: String)
fun onError(errorMessage: String) fun onError(errorMessage: String)
fun onStartStreaming(from: String, For: String, channel: String) fun onStartStreaming(from: String, For: String, channel: String)
fun onStopStreaming(from: String, For: String, channel: String) fun onStopStreaming(from: String, For: String, channel: String)