Compare commits

..

6 Commits

Author SHA1 Message Date
8c32e48e04 Commit 12/08/2025 2025-08-12 16:48:36 +07:00
60e8524c8f Commit 12/08/2025 2025-08-12 09:44:11 +07:00
f4c9fa8730 Commit 07/08/2025 2025-08-07 15:12:36 +07:00
3b31044610 Commit 07/08/2025 2025-08-07 15:11:52 +07:00
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
49 changed files with 2165 additions and 560 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/

20
.idea/artifacts/EWS_Nanopi_Duo2.xml generated Normal file
View File

@@ -0,0 +1,20 @@
<component name="ArtifactManager">
<artifact name="EWS Nanopi Duo2">
<output-path>$PROJECT_DIR$/out/artifacts/EWS_Nanopi_Duo2</output-path>
<root id="root">
<element id="archive" name="EWS_POC.jar">
<element id="directory" name="META-INF">
<element id="file-copy" path="$PROJECT_DIR$/meta/nanopi duo2/META-INF/MANIFEST.MF" />
</element>
<element id="module-output" name="EWS_POC" />
</element>
<element id="library" level="project" name="jetbrains.kotlinx.coroutines.core" />
<element id="library" level="project" name="java.websocket.Java.WebSocket" />
<element id="library" level="project" name="KotlinJavaRuntime" />
<element id="library" level="project" name="net.java.dev.jna" />
<element id="library" level="project" name="io.javalin.bundle" />
<element id="dir-copy" path="$PROJECT_DIR$/html" />
<element id="dir-copy" path="$PROJECT_DIR$/libs/linux-arm" />
</root>
</artifact>
</component>

15
.idea/deployment.xml generated Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PublishConfigData" serverName="nanopi gtcdevice01" remoteFilesAllowedToDisappearOnAutoupload="false" confirmBeforeUploading="false">
<option name="confirmBeforeUploading" value="false" />
<serverData>
<paths name="nanopi gtcdevice01">
<serverdata>
<mappings>
<mapping deploy="/EWS_POC" local="$PROJECT_DIR$" web="/" />
</mappings>
</serverdata>
</paths>
</serverData>
</component>
</project>

View File

@@ -4,6 +4,7 @@
<inspection_tool class="AiaStyle" enabled="false" level="TYPO" enabled_by_default="false" /> <inspection_tool class="AiaStyle" enabled="false" level="TYPO" enabled_by_default="false" />
<inspection_tool class="AssignedValueIsNeverRead" enabled="false" level="WARNING" enabled_by_default="false" /> <inspection_tool class="AssignedValueIsNeverRead" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ClassName" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> <inspection_tool class="ClassName" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ConstPropertyName" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> <inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="FunctionName" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> <inspection_tool class="FunctionName" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="GrazieInspection" enabled="false" level="GRAMMAR_ERROR" enabled_by_default="false" /> <inspection_tool class="GrazieInspection" enabled="false" level="GRAMMAR_ERROR" enabled_by_default="false" />

8
.idea/sshConfigs.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SshConfigs">
<configs>
<sshConfig authType="PASSWORD" host="100.64.0.1" id="fffd62d4-dc18-4867-bb4a-ffee6db55e4d" port="22" nameFormat="DESCRIPTIVE" username="gtcdev" useOpenSSHConfig="true" />
</configs>
</component>
</project>

14
.idea/webServers.xml generated Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="WebServers">
<option name="servers">
<webServer id="72781508-f367-4872-b931-5138bc85e8bd" name="nanopi gtcdevice01">
<fileTransfer rootFolder="/home/gtcdev" accessType="SFTP" host="100.64.0.1" port="22" sshConfigId="fffd62d4-dc18-4867-bb4a-ffee6db55e4d" sshConfig="gtcdev@100.64.0.1:22 password">
<advancedOptions>
<advancedOptions dataProtectionLevel="Private" keepAliveTimeout="0" passiveMode="true" shareSSLContext="true" isUseRsync="true" />
</advancedOptions>
</fileTransfer>
</webServer>
</option>
</component>
</project>

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,87 +0,0 @@
$(document).ready(function() {
// Your code here
console.log('precordedbroadcast.js is ready!');
const path = window.location.pathname;
const ws = new WebSocket('ws://' + window.location.host + path + '/ws');
for(let i = 1; i<=8; i++){
$(`#fileM${i}`).val('');
$(`#playM${i}`).prop('disabled', true);
$(`#stopM${i}`).prop('disabled', true);
}
ws.onopen = function() {
console.log('WebSocket connection opened');
$('#indicatorDisconnected').addClass('visually-hidden');
$('#indicatorConnected').removeClass('visually-hidden');
sendCommand({ command: "getMessageConfig" });
setInterval(function() {
sendCommand({ command: "getPlaybackStatus" });
}, 5000);
};
ws.onmessage = function(event) {
console.log('WebSocket message received:', event.data);
let msg = {};
try {
msg = JSON.parse(event.data);
} catch (e) {
return;
}
if (msg.reply === "getMessageConfig" && msg.data !== undefined) {
const messageConfigdata = msg.data;
console.log('Message Config Data:', messageConfigdata);
for(let i=1; i<=8; i++){
let filetitle = $(`#fileM${i}`);
let playButton = $(`#playM${i}`);
let stopButton = $(`#stopM${i}`);
let fileInput = messageConfigdata[`M${i}`] || '';
filetitle.val(fileInput);
if (fileInput.length>0){
playButton.prop('disabled', false);
stopButton.prop('disabled', false);
playButton.on('click', function() {
let cmd = {
command: "playMessage",
data: `M${i}`
}
sendCommand(cmd);
});
stopButton.on('click', function() {
let cmd = {
command: "stopMessage",
data: `M${i}`
}
sendCommand(cmd);
});
} else {
playButton.prop('disabled', true);
stopButton.prop('disabled', true);
}
}
} else if (msg.reply === "getPlaybackStatus" && msg.data !== undefined && msg.data.length > 0) {
const playbackData = msg.data;
$('#playbackStatus').text(playbackData.status);
}
};
ws.onclose = function() {
console.log('WebSocket connection closed');
$('#indicatorDisconnected').removeClass('visually-hidden');
$('#indicatorConnected').addClass('visually-hidden');
};
ws.onerror = function(error) {
console.error('WebSocket error:', error);
};
function sendCommand(cmd) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(cmd));
} else {
console.error('WebSocket is not open. Unable to send command:', JSON.stringify(cmd));
}
}
});

View File

@@ -1,184 +0,0 @@
$(document).ready(function() {
// Your initialization code here
console.log('setting.js is ready!');
$('#dropdownM1 button').text('');
$('#dropdownM2 button').text('');
$('#dropdownM3 button').text('');
$('#dropdownM4 button').text('');
$('#dropdownM5 button').text('');
$('#dropdownM6 button').text('');
$('#dropdownM7 button').text('');
$('#dropdownM8 button').text('');
$('#dropdownM1 .dropdown-menu').empty();
$('#dropdownM2 .dropdown-menu').empty();
$('#dropdownM3 .dropdown-menu').empty();
$('#dropdownM4 .dropdown-menu').empty();
$('#dropdownM5 .dropdown-menu').empty();
$('#dropdownM6 .dropdown-menu').empty();
$('#dropdownM7 .dropdown-menu').empty();
$('#dropdownM8 .dropdown-menu').empty();
// Connect to WebSocket at "<current html file>/ws"
const path = window.location.pathname;
const ws = new WebSocket('ws://' + window.location.host + path + '/ws');
ws.onopen = function() {
console.log('WebSocket connection opened');
$('#indicatorDisconnected').addClass('visually-hidden');
$('#indicatorConnected').removeClass('visually-hidden');
sendCommand({ command: "getConfig" });
};
ws.onmessage = function(event) {
console.log('WebSocket message received:', event.data);
let msg = {};
try {
msg = JSON.parse(event.data);
} catch (e) {
return;
}
if (msg.reply === "getConfig" && msg.data !== undefined ) {
const configData = JSON.parse(msg.data);
console.log('Config Data:', configData);
$('#zelloUsername').val(configData.zelloUsername || '');
$('#zelloPassword').val(configData.zelloPassword || '');
$('#zelloChannel').val(configData.zelloChannel || '');
if ("community" === configData.zelloServer) {
$('#zellocommunity').prop('checked', true);
$('#zelloWorkNetworkName').val('').prop('disabled', true);
$('#zelloEnterpriseServerDomain').val('').prop('disabled', true);
} else if ("work" === configData.zelloServer) {
$('#zellowork').prop('checked', true);
$('#zelloWorkNetworkName').val(configData.zelloWorkNetworkName || '').prop('disabled', false);
$('#zelloEnterpriseServerDomain').val('').prop('disabled', true);
} else if ("enterprise" === configData.zelloServer) {
$('#zelloenterprise').prop('checked', true);
$('#zelloWorkNetworkName').val('').prop('disabled', true);
$('#zelloEnterpriseServerDomain').val(configData.zelloEnterpriseServerDomain || '').prop('disabled', false);
}
for (let i = 1; i <= 8; i++) {
const dropdownMenu = $(`#dropdownM${i} .dropdown-menu`);
const dropdownButton = $(`#dropdownM${i} button`);
dropdownMenu.empty();
const messages = configData[`MessageList`] || [];
messages.forEach((msg, idx) => {
const item = $('<a class="dropdown-item" href="#"></a>').text(msg);
item.on('click', function() {
dropdownButton.text(msg);
});
dropdownMenu.append(item);
});
// Set button text to selected message if present
if (configData[`m${i}`]) {
dropdownButton.text(configData[`m${i}`]);
} else {
dropdownButton.text('');
}
}
}
};
ws.onclose = function() {
console.log('WebSocket connection closed');
$('#indicatorDisconnected').removeClass('visually-hidden');
$('#indicatorConnected').addClass('visually-hidden');
};
ws.onerror = function(error) {
console.error('WebSocket error:', error);
};
$('#zellocommunity').on('click', function() {
$('#zelloWorkNetworkName').val('').prop('disabled', true);
$('#zelloEnterpriseServerDomain').val('').prop('disabled', true);
});
$('#zellowork').on('click', function() {
$('#zelloWorkNetworkName').prop('disabled', false);
$('#zelloEnterpriseServerDomain').val('').prop('disabled', true);
});
$('#zelloenterprise').on('click', function() {
$('#zelloWorkNetworkName').val('').prop('disabled', true);
$('#zelloEnterpriseServerDomain').prop('disabled', false);
});
$('#btnApplyZello').on('click', function() {
if (ws.readyState === WebSocket.OPEN) {
let data = {
command: "setZelloConfig",
ZelloUsername: $('#zelloUsername').val(),
ZelloPassword: $('#zelloPassword').val(),
ZelloChannel: $('#zelloChannel').val(),
ZelloServer: $('#zellocommunity').is(':checked') ? 'community' :
$('#zellowork').is(':checked') ? 'work' :
$('#zelloenterprise').is(':checked') ? 'enterprise' : ''
};
if ($('#zellowork').is(':checked')) {
data.ZelloWorkNetworkName = $('#zelloWorkNetworkName').val();
}
if ($('#zelloenterprise').is(':checked')) {
data.ZelloEnterpriseServerDomain = $('#zelloEnterpriseServerDomain').val();
}
sendCommand(data);
} else {
console.warn('WebSocket is not open.');
}
});
$('#btnApplyMessage').on('click', function() {
if (ws.readyState === WebSocket.OPEN) {
let data = {
command: "setMessageConfig",
M1: $('#dropdownM1 button').text(),
M2: $('#dropdownM2 button').text(),
M3: $('#dropdownM3 button').text(),
M4: $('#dropdownM4 button').text(),
M5: $('#dropdownM5 button').text(),
M6: $('#dropdownM6 button').text(),
M7: $('#dropdownM7 button').text(),
M8: $('#dropdownM8 button').text()
};
sendCommand(data);
} else {
console.warn('WebSocket is not open.');
}
});
$('#btnUploadContent').on('click', function() {
const fileInput = document.getElementById('chosenFile');
if (!fileInput || !fileInput.files.length) {
alert('Please select a file to upload.');
return;
}
const file = fileInput.files[0];
const formData = new FormData();
formData.append('file', file);
fetch(window.location.pathname + '/upload', {
method: 'POST',
body: formData
})
.then(response => {
if (response.ok) {
alert('File uploaded successfully.');
} else {
alert('File upload failed.');
}
})
.catch(error => {
console.error('Upload error:', error);
alert('An error occurred during upload.');
});
});
function sendCommand(cmd) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(cmd));
} else {
console.error('WebSocket is not open. Unable to send command:', JSON.stringify(cmd));
}
}
});

View File

@@ -0,0 +1,109 @@
$(document).ready(function() {
const path = window.location.pathname;
const ws = new WebSocket('ws://' + window.location.host + path + '/ws');
ws.onopen = function() {
//console.log('WebSocket connection opened');
$('#indicatorDisconnected').addClass('visually-hidden');
$('#indicatorConnected').removeClass('visually-hidden');
setInterval(function() {
sendCommand({ command: "getCPUStatus" });
sendCommand({ command: "getMemoryStatus" });
sendCommand({ command: "getDiskStatus" });
sendCommand({ command: "getEthernetStatus" });
sendCommand({ command: "getModemStatus" });
sendCommand({ command: "getTunnelStatus" });
}, 1000);
};
ws.onmessage = function(event) {
//console.log('WebSocket message received:', event.data);
let msg = {};
try {
msg = JSON.parse(event.data);
} catch (e) {
console.error('Invalid JSON:', event.data);
return;
}
if (msg.reply && msg.reply.length>0 && msg.data && msg.data.length > 0) {
switch (msg.reply) {
case "getCPUStatus":
const cpuData = JSON.parse(msg.data);
//console.log('CPU Status Data:', cpuData);
$('#cpuUsage').text(cpuData.average ? cpuData.average + '%' : 'N/A');
$('#core0Usage').text(cpuData.core0 ? cpuData.core0 + '%' : 'N/A');
$('#core1Usage').text(cpuData.core1 ? cpuData.core1 + '%' : 'N/A');
$('#core2Usage').text(cpuData.core2 ? cpuData.core2 + '%' : 'N/A');
$('#core3Usage').text(cpuData.core3 ? cpuData.core3 + '%' : 'N/A');
$('#cpuTemperature').text(cpuData.temperature ? cpuData.temperature + '°C' : 'N/A');
break;
case "getMemoryStatus":
const memoryData = JSON.parse(msg.data);
//console.log('Memory Status Data:', memoryData);
$('#ramTotal').text(memoryData.total ? memoryData.total : 'N/A');
$('#ramUsed').text(memoryData.used ? memoryData.used : 'N/A');
$('#ramFree').text(memoryData.free ? memoryData.free : 'N/A');
$('#ramFreePercent').text(memoryData.freePercent ? memoryData.freePercent + '%' : 'N/A');
break;
case "getDiskStatus":
const diskData = JSON.parse(msg.data);
//console.log('Disk Status Data:', diskData);
$('#diskTotal').text(diskData.total ? diskData.total : 'N/A');
$('#diskUsed').text(diskData.used ? diskData.used : 'N/A');
$('#diskFree').text(diskData.free ? diskData.free : 'N/A');
$('#diskFreePercent').text(diskData.freePercent ? diskData.freePercent + '%' : 'N/A');
break;
case "getEthernetStatus":
const ethernetData = JSON.parse(msg.data);
//console.log('Ethernet Status Data:', ethernetData);
$('#eth0Status').text(ethernetData.status ? ethernetData.status : 'N/A');
$('#eth0IP').text(ethernetData.ip ? ethernetData.ip : 'N/A');
$('#eth0TXBytes').text(ethernetData.txBytes ? ethernetData.txBytes : 'N/A');
$('#eth0RXBytes').text(ethernetData.rxBytes ? ethernetData.rxBytes : 'N/A');
$('#eth0TXSpeed').text(ethernetData.txSpeed ? ethernetData.txSpeed : 'N/A');
$('#eth0RXSpeed').text(ethernetData.rxSpeed ? ethernetData.rxSpeed : 'N/A');
break;
case "getModemStatus":
const modemData = JSON.parse(msg.data);
//console.log('Modem Status Data:', modemData);
$('#modemStatus').text(modemData.status ? modemData.status : 'N/A');
$('#modemIP').text(modemData.ip ? modemData.ip : 'N/A');
$('#modemTXBytes').text(modemData.txBytes ? modemData.txBytes : 'N/A');
$('#modemRXBytes').text(modemData.rxBytes ? modemData.rxBytes : 'N/A');
$('#modemTXSpeed').text(modemData.txSpeed ? modemData.txSpeed : 'N/A');
$('#modemRXSpeed').text(modemData.rxSpeed ? modemData.rxSpeed : 'N/A');
break;
case "getTunnelStatus":
const tunnelData = JSON.parse(msg.data);
//console.log('Tunnel Status Data:', tunnelData);
$('#tunnelStatus').text(tunnelData.status ? tunnelData.status : 'N/A');
$('#tunnelIP').text(tunnelData.ip ? tunnelData.ip : 'N/A');
$('#tunnelTXBytes').text(tunnelData.txBytes ? tunnelData.txBytes : 'N/A');
$('#tunnelRXBytes').text(tunnelData.rxBytes ? tunnelData.rxBytes : 'N/A');
$('#tunnelTXSpeed').text(tunnelData.txSpeed ? tunnelData.txSpeed : 'N/A');
$('#tunnelRXSpeed').text(tunnelData.rxSpeed ? tunnelData.rxSpeed : 'N/A');
break;
}
}
};
ws.onclose = function() {
//console.log('WebSocket connection closed');
$('#indicatorDisconnected').removeClass('visually-hidden');
$('#indicatorConnected').addClass('visually-hidden');
};
ws.onerror = function(error) {
console.error('WebSocket error:', error);
};
function sendCommand(command) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(command));
} else {
console.error('WebSocket is not open. Unable to send command:', command);
}
}
});

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

@@ -0,0 +1,114 @@
$(document).ready(function () {
// Your code here
//console.log('precordedbroadcast.js is ready!');
const path = window.location.pathname;
const ws = new WebSocket('ws://' + window.location.host + path + '/ws');
for (let i = 1; i <= 8; i++) {
$(`#fileM${i}`).val('');
$(`#playM${i}`).prop('disabled', true);
$(`#stopM${i}`).prop('disabled', true);
}
ws.onopen = function () {
//console.log('WebSocket connection opened');
$('#indicatorDisconnected').addClass('visually-hidden');
$('#indicatorConnected').removeClass('visually-hidden');
sendCommand({ command: "getMessageConfig" });
setInterval(function () {
sendCommand({ command: "getPlaybackStatus" });
}, 1000); // every second
};
ws.onmessage = function (event) {
//console.log('WebSocket message received:', event.data);
let msg = {};
try {
msg = JSON.parse(event.data);
} catch (e) {
return;
}
if (msg.reply && msg.reply.length > 0 && msg.data && msg.data.length > 0) {
switch (msg.reply) {
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 playButton = $(`#playM${i}`);
let stopButton = $(`#stopM${i}`);
let fileInput = messageConfigdata[`M${i}`];
if (fileInput && fileInput.length > 0) {
filetitle.text(fileInput);
playButton.prop('disabled', false);
stopButton.prop('disabled', false);
playButton.removeClass('invisible');
stopButton.removeClass('invisible');
playButton.on('click', function () {
let cmd = {
command: "playMessage",
data: `M${i}`
}
sendCommand(cmd);
});
stopButton.on('click', function () {
let cmd = {
command: "stopMessage",
data: `M${i}`
}
sendCommand(cmd);
});
} else {
filetitle.text('Not configured');
playButton.addClass('invisible');
stopButton.addClass('invisible');
}
}
break;
case "getPlaybackStatus":
$('#playbackStatus').text(msg.data);
break;
}
}
};
ws.onclose = function () {
//console.log('WebSocket connection closed');
$('#indicatorDisconnected').removeClass('visually-hidden');
$('#indicatorConnected').addClass('visually-hidden');
};
ws.onerror = function (error) {
console.error('WebSocket error:', error);
};
function sendCommand(cmd) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(cmd));
} else {
console.error('WebSocket is not open. Unable to send command:', JSON.stringify(cmd));
}
}
});

View File

@@ -0,0 +1,212 @@
$(document).ready(function () {
// Your initialization code here
//console.log('setting.js is ready!');
$('#dropdownM1 button').text('');
$('#dropdownM2 button').text('');
$('#dropdownM3 button').text('');
$('#dropdownM4 button').text('');
$('#dropdownM5 button').text('');
$('#dropdownM6 button').text('');
$('#dropdownM7 button').text('');
$('#dropdownM8 button').text('');
$('#dropdownM1 .dropdown-menu').empty();
$('#dropdownM2 .dropdown-menu').empty();
$('#dropdownM3 .dropdown-menu').empty();
$('#dropdownM4 .dropdown-menu').empty();
$('#dropdownM5 .dropdown-menu').empty();
$('#dropdownM6 .dropdown-menu').empty();
$('#dropdownM7 .dropdown-menu').empty();
$('#dropdownM8 .dropdown-menu').empty();
// Connect to WebSocket at "<current html file>/ws"
const path = window.location.pathname;
const ws = new WebSocket('ws://' + window.location.host + path + '/ws');
ws.onopen = function () {
//console.log('WebSocket connection opened');
$('#indicatorDisconnected').addClass('visually-hidden');
$('#indicatorConnected').removeClass('visually-hidden');
sendCommand({ command: "getConfig" });
};
ws.onmessage = function (event) {
//console.log('WebSocket message received:', event.data);
let msg = {};
try {
msg = JSON.parse(event.data);
} catch (e) {
return;
}
if (msg.reply && msg.reply.length > 0) {
switch (msg.reply) {
case "getConfig":
const configData = JSON.parse(msg.data);
//console.log('Config Data:', configData);
$('#zelloUsername').val(configData.zelloUsername || '');
$('#zelloPassword').val(configData.zelloPassword || '');
$('#zelloChannel').val(configData.zelloChannel || '');
if ("community" === configData.zelloServer) {
$('#zellocommunity').prop('checked', true);
$('#zelloWorkNetworkName').val('').prop('disabled', true);
$('#zelloEnterpriseServerDomain').val('').prop('disabled', true);
} else if ("work" === configData.zelloServer) {
$('#zellowork').prop('checked', true);
$('#zelloWorkNetworkName').val(configData.zelloWorkNetworkName || '').prop('disabled', false);
$('#zelloEnterpriseServerDomain').val('').prop('disabled', true);
} else if ("enterprise" === configData.zelloServer) {
$('#zelloenterprise').prop('checked', true);
$('#zelloWorkNetworkName').val('').prop('disabled', true);
$('#zelloEnterpriseServerDomain').val(configData.zelloEnterpriseServerDomain || '').prop('disabled', false);
}
for (let i = 1; i <= 8; i++) {
const dropdownMenu = $(`#dropdownM${i} .dropdown-menu`);
const dropdownButton = $(`#dropdownM${i} button`);
dropdownMenu.empty();
const messages = configData[`messageList`] || [];
messages.forEach((msg, idx) => {
const item = $('<a class="dropdown-item" href="#"></a>').text(msg);
item.on('click', function () {
dropdownButton.text(msg);
});
dropdownMenu.append(item);
});
// Set button text to selected message if present
if (configData[`m${i}`]) {
dropdownButton.text(configData[`m${i}`]);
} else {
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 () {
//console.log('WebSocket connection closed');
$('#indicatorDisconnected').removeClass('visually-hidden');
$('#indicatorConnected').addClass('visually-hidden');
};
ws.onerror = function (error) {
console.error('WebSocket error:', error);
};
$('#zellocommunity').on('click', function () {
$('#zelloWorkNetworkName').val('').prop('disabled', true);
$('#zelloEnterpriseServerDomain').val('').prop('disabled', true);
});
$('#zellowork').on('click', function () {
$('#zelloWorkNetworkName').prop('disabled', false);
$('#zelloEnterpriseServerDomain').val('').prop('disabled', true);
});
$('#zelloenterprise').on('click', function () {
$('#zelloWorkNetworkName').val('').prop('disabled', true);
$('#zelloEnterpriseServerDomain').prop('disabled', false);
});
$('#btnApplyZello').on('click', function () {
if (ws.readyState === WebSocket.OPEN) {
let xx = {
command: "setZelloConfig"
};
let data = {
ZelloUsername: $('#zelloUsername').val(),
ZelloPassword: $('#zelloPassword').val(),
ZelloChannel: $('#zelloChannel').val(),
ZelloServer: $('#zellocommunity').is(':checked') ? 'community' :
$('#zellowork').is(':checked') ? 'work' :
$('#zelloenterprise').is(':checked') ? 'enterprise' : ''
}
if ($('#zellowork').is(':checked')) {
data.ZelloWorkNetworkName = $('#zelloWorkNetworkName').val();
}
if ($('#zelloenterprise').is(':checked')) {
data.ZelloEnterpriseServerDomain = $('#zelloEnterpriseServerDomain').val();
}
xx.data = JSON.stringify(data);
sendCommand(xx);
} else {
console.warn('WebSocket is not open.');
}
});
$('#btnApplyMessage').on('click', function () {
if (ws.readyState === WebSocket.OPEN) {
let data = {
command: "setMessageConfig",
data: JSON.stringify({
M1: $('#dropdownM1 button').text(),
M2: $('#dropdownM2 button').text(),
M3: $('#dropdownM3 button').text(),
M4: $('#dropdownM4 button').text(),
M5: $('#dropdownM5 button').text(),
M6: $('#dropdownM6 button').text(),
M7: $('#dropdownM7 button').text(),
M8: $('#dropdownM8 button').text()
})
};
sendCommand(data);
} else {
console.warn('WebSocket is not open.');
}
});
$('#btnUploadContent').on('click', function () {
const fileInput = document.getElementById('chosenFile');
if (!fileInput || !fileInput.files.length) {
alert('Please select a file to upload.');
return;
}
const file = fileInput.files[0];
const formData = new FormData();
formData.append('file', file);
fetch(window.location.pathname + '/upload', {
method: 'POST',
body: formData
})
.then(response => {
if (response.ok) {
alert('File uploaded successfully.');
sendCommand({ command: "getConfig" }); // Refresh config after upload
} else {
alert('File upload failed.');
}
})
.catch(error => {
console.error('Upload error:', error);
alert('An error occurred during upload.');
});
});
function sendCommand(cmd) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(cmd));
} else {
console.error('WebSocket is not open. Unable to send command:', JSON.stringify(cmd));
}
}
});

View File

@@ -0,0 +1,260 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>EWS_POC</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/Navbar-With-Button-icons.css">
</head>
<body>
<nav class="navbar navbar-expand-md bg-body py-3">
<div class="container"><a class="navbar-brand d-flex align-items-center" href="#"><span class="bs-icon-sm bs-icon-rounded bs-icon-primary d-flex justify-content-center align-items-center me-2 bs-icon"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-bezier">
<path fill-rule="evenodd" d="M0 10.5A1.5 1.5 0 0 1 1.5 9h1A1.5 1.5 0 0 1 4 10.5v1A1.5 1.5 0 0 1 2.5 13h-1A1.5 1.5 0 0 1 0 11.5zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm10.5.5A1.5 1.5 0 0 1 13.5 9h1a1.5 1.5 0 0 1 1.5 1.5v1a1.5 1.5 0 0 1-1.5 1.5h-1a1.5 1.5 0 0 1-1.5-1.5zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zM6 4.5A1.5 1.5 0 0 1 7.5 3h1A1.5 1.5 0 0 1 10 4.5v1A1.5 1.5 0 0 1 8.5 7h-1A1.5 1.5 0 0 1 6 5.5zM7.5 4a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5z"></path>
<path d="M6 4.5H1.866a1 1 0 1 0 0 1h2.668A6.517 6.517 0 0 0 1.814 9H2.5c.123 0 .244.015.358.043a5.517 5.517 0 0 1 3.185-3.185A1.503 1.503 0 0 1 6 5.5zm3.957 1.358A1.5 1.5 0 0 0 10 5.5v-1h4.134a1 1 0 1 1 0 1h-2.668a6.517 6.517 0 0 1 2.72 3.5H13.5c-.123 0-.243.015-.358.043a5.517 5.517 0 0 0-3.185-3.185z"></path>
</svg></span><span>Early Warning System Receiver</span></a><button data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-1"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
<div class="collapse navbar-collapse" id="navcol-1">
<ul class="navbar-nav me-auto">
<li class="nav-item"><a class="nav-link" href="pocreceiver.html">POC Receiver</a></li>
<li class="nav-item"><a class="nav-link" href="prerecordedbroadcast.html">Pre-Recorded Broadcast</a></li>
<li class="nav-item"><a class="nav-link" href="setting.html">Setting and Content</a></li>
<li class="nav-item"><a class="nav-link active" href="#">Hardware Status</a></li>
</ul><a class="btn btn-primary" role="button" href="index.html">Logout<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor" class="visually-hidden" id="indicatorDisconnected" style="color: var(--bs-danger);width: 26px;height: 26px;font-weight: bold;">
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
<path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c-9.4 9.4-9.4 24.6 0 33.9l47 47-47 47c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l47-47 47 47c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-47-47 47-47c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-47 47-47-47c-9.4-9.4-24.6-9.4-33.9 0z"></path>
</svg><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor" class="visually-hidden" id="indicatorConnected" style="color: var(--bs-success);width: 26px;height: 26px;font-weight: bold;">
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
<path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-111 111-47-47c-9.4-9.4-24.6-9.4-33.9 0s-9.4 24.6 0 33.9l64 64c9.4 9.4 24.6 9.4 33.9 0L369 209z"></path>
</svg></a>
</div>
</div>
</nav>
<div class="container">
<div class="row">
<h1 class="text-center">Hardware Status</h1>
</div>
<div class="row">
<div class="col-6 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">CPU (Average)</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">C0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">C1</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">C2</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">C3</p>
</div>
<div class="col-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">Temp</p>
</div>
</div>
<div class="row">
<div class="col-6 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="cpuUsage">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="core0Usage">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="core1Usage">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="core2Usage">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="core3Usage">0.0</p>
</div>
<div class="col-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="cpuTemp">0</p>
</div>
</div>
<div class="row">
<div class="col-6 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">RAM Total</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">Free</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">Used</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">Free %</p>
</div>
</div>
<div class="row">
<div class="col-6 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="ramTotal">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="ramFree">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="ramUsed">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="ramFreePercent">0.0</p>
</div>
</div>
<div class="row">
<div class="col-6 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">Disk Total</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">Free</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">Used</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">Free %</p>
</div>
</div>
<div class="row">
<div class="col-6 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="diskTotal">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="diskFree">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="diskUsed">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="diskFreePercent">0.0</p>
</div>
</div>
<div class="row">
<div class="col-6 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">ETH 0 Status</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">IP</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">TX Bytes</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">RX Bytes</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">TX Speed</p>
</div>
<div class="col-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">RX Speed</p>
</div>
</div>
<div class="row">
<div class="col-6 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="eth0Status">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="eth0IP">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="eth0TXBytes">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="eth0RXBytes">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="eth0TXSpeed">0.0</p>
</div>
<div class="col-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="eth0RXSpeed">0</p>
</div>
</div>
<div class="row">
<div class="col-6 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">Modem Status</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">IP</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">TX Bytes</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">RX Bytes</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">TX Speed</p>
</div>
<div class="col-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">RX Speed</p>
</div>
</div>
<div class="row">
<div class="col-6 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="modemStatus">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="modemIP">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="modemTXBytes">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="modemRXBytes">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="modemTXSpeed">0.0</p>
</div>
<div class="col-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="modemRXSpeed">0</p>
</div>
</div>
<div class="row">
<div class="col-6 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">Tunnel Status</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">IP</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">TX Bytes</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">RX Bytes</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">TX Speed</p>
</div>
<div class="col-2 col-md-2">
<p class="fw-bold d-flex h-100 justify-content-center align-items-center">RX Speed</p>
</div>
</div>
<div class="row">
<div class="col-6 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="tunnelStatus">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="tunnelIP">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="tunnelTXBytes">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="tunnelRXBytes">0.0</p>
</div>
<div class="col-1 col-sm-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="tunnelTXSpeed">0.0</p>
</div>
<div class="col-2 col-md-2">
<p class="d-flex h-100 justify-content-center align-items-center" id="tunnelRXSpeed">0</p>
</div>
</div>
</div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/js/jquery-3.7.1.min.js"></script>
<script src="assets/js/hardwarestatus.js"></script>
</body>
</html>

View File

@@ -20,6 +20,7 @@
<li class="nav-item"><a class="nav-link active" href="#">POC Receiver</a></li> <li class="nav-item"><a class="nav-link active" href="#">POC Receiver</a></li>
<li class="nav-item"><a class="nav-link" href="prerecordedbroadcast.html">Pre-Recorded Broadcast</a></li> <li class="nav-item"><a class="nav-link" href="prerecordedbroadcast.html">Pre-Recorded Broadcast</a></li>
<li class="nav-item"><a class="nav-link" href="setting.html">Setting and Content</a></li> <li class="nav-item"><a class="nav-link" href="setting.html">Setting and Content</a></li>
<li class="nav-item"><a class="nav-link" href="hardwarestatus.html">Hardware Status</a></li>
</ul><a class="btn btn-primary" role="button" href="index.html">Logout<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor" class="visually-hidden" id="indicatorDisconnected" style="color: var(--bs-danger);width: 26px;height: 26px;font-weight: bold;"> </ul><a class="btn btn-primary" role="button" href="index.html">Logout<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor" class="visually-hidden" id="indicatorDisconnected" style="color: var(--bs-danger);width: 26px;height: 26px;font-weight: bold;">
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --> <!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
<path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c-9.4 9.4-9.4 24.6 0 33.9l47 47-47 47c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l47-47 47 47c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-47-47 47-47c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-47 47-47-47c-9.4-9.4-24.6-9.4-33.9 0z"></path> <path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c-9.4 9.4-9.4 24.6 0 33.9l47 47-47 47c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l47-47 47 47c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-47-47 47-47c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-47 47-47-47c-9.4-9.4-24.6-9.4-33.9 0z"></path>
@@ -35,7 +36,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">No Status</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

@@ -20,6 +20,7 @@
<li class="nav-item"><a class="nav-link" href="pocreceiver.html">POC Receiver</a></li> <li class="nav-item"><a class="nav-link" href="pocreceiver.html">POC Receiver</a></li>
<li class="nav-item"><a class="nav-link active" href="#">Pre-Recorded Broadcast</a></li> <li class="nav-item"><a class="nav-link active" href="#">Pre-Recorded Broadcast</a></li>
<li class="nav-item"><a class="nav-link" href="setting.html">Setting and Content</a></li> <li class="nav-item"><a class="nav-link" href="setting.html">Setting and Content</a></li>
<li class="nav-item"><a class="nav-link" href="hardwarestatus.html">Hardware Status</a></li>
</ul><a class="btn btn-primary" role="button" href="index.html">Logout<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor" class="visually-hidden" id="indicatorDisconnected" style="color: var(--bs-danger);width: 26px;height: 26px;font-weight: bold;"> </ul><a class="btn btn-primary" role="button" href="index.html">Logout<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor" class="visually-hidden" id="indicatorDisconnected" style="color: var(--bs-danger);width: 26px;height: 26px;font-weight: bold;">
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --> <!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
<path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c-9.4 9.4-9.4 24.6 0 33.9l47 47-47 47c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l47-47 47 47c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-47-47 47-47c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-47 47-47-47c-9.4-9.4-24.6-9.4-33.9 0z"></path> <path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c-9.4 9.4-9.4 24.6 0 33.9l47 47-47 47c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l47-47 47 47c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-47-47 47-47c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-47 47-47-47c-9.4-9.4-24.6-9.4-33.9 0z"></path>
@@ -43,7 +44,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 +56,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 +70,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 +82,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 +96,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 +108,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 +122,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 +134,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

@@ -20,6 +20,7 @@
<li class="nav-item"><a class="nav-link" href="pocreceiver.html">POC Receiver</a></li> <li class="nav-item"><a class="nav-link" href="pocreceiver.html">POC Receiver</a></li>
<li class="nav-item"><a class="nav-link" href="prerecordedbroadcast.html">Pre-Recorded Broadcast</a></li> <li class="nav-item"><a class="nav-link" href="prerecordedbroadcast.html">Pre-Recorded Broadcast</a></li>
<li class="nav-item"><a class="nav-link active" href="#">Setting and Content</a></li> <li class="nav-item"><a class="nav-link active" href="#">Setting and Content</a></li>
<li class="nav-item"><a class="nav-link" href="hardwarestatus.html">Hardware&nbsp; Status</a></li>
</ul><a class="btn btn-primary" role="button" href="index.html">Logout<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor" class="visually-hidden" id="indicatorDisconnected" style="color: var(--bs-danger);width: 26px;height: 26px;font-weight: bold;"> </ul><a class="btn btn-primary" role="button" href="index.html">Logout<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor" class="visually-hidden" id="indicatorDisconnected" style="color: var(--bs-danger);width: 26px;height: 26px;font-weight: bold;">
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --> <!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
<path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c-9.4 9.4-9.4 24.6 0 33.9l47 47-47 47c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l47-47 47 47c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-47-47 47-47c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-47 47-47-47c-9.4-9.4-24.6-9.4-33.9 0z"></path> <path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c-9.4 9.4-9.4 24.6 0 33.9l47 47-47 47c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l47-47 47 47c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-47-47 47-47c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-47 47-47-47c-9.4-9.4-24.6-9.4-33.9 0z"></path>
@@ -168,14 +169,9 @@
<div class="row w-100"> <div class="row w-100">
<h1 class="text-center">Content Upload</h1> <h1 class="text-center">Content Upload</h1>
</div> </div>
<div class="row"><input type="file" id="chosenFile"></div> <div class="row w-100">
<div class="row"> <div class="col-10"><input type="file" id="chosenFile"></div>
<div class="col"> <div class="col-2"><button class="btn btn-primary" id="btnUploadContent" type="button">Upload</button></div>
<div class="progress h-100" id="contentUploadProgress">
<div class="progress-bar" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" style="width: 50%;">50%</div>
</div>
</div>
<div class="col-2"><button class="btn btn-primary mt-2 mb-2" id="btnUploadContent" type="button">Upload</button></div>
</div> </div>
</div> </div>
<script src="assets/bootstrap/js/bootstrap.min.js"></script> <script src="assets/bootstrap/js/bootstrap.min.js"></script>

View File

@@ -0,0 +1,27 @@
Manifest-Version: 1.0
Main-Class: MainKt
Class-Path: kotlinx-coroutines-core-1.10.2.jar kotlinx-coroutines-core-j
vm-1.10.2.jar annotations-23.0.0.jar kotlin-stdlib-2.1.0.jar Java-WebSo
cket-1.6.0.jar slf4j-api-2.0.13.jar kotlin-stdlib-2.2.0.jar annotations
-13.0.jar jna-5.17.0.jar javalin-bundle-6.7.0.jar javalin-6.7.0.jar slf
4j-api-2.0.17.jar jetty-server-11.0.25.jar jetty-http-11.0.25.jar webso
cket-jetty-server-11.0.25.jar jetty-servlet-11.0.25.jar jetty-security-
11.0.25.jar jetty-webapp-11.0.25.jar jetty-xml-11.0.25.jar websocket-je
tty-api-11.0.25.jar websocket-jetty-common-11.0.25.jar websocket-core-c
ommon-11.0.25.jar websocket-servlet-11.0.25.jar websocket-core-server-1
1.0.25.jar javalin-context-mock-6.7.0.jar jetty-jakarta-servlet-api-5.0
.2.jar javalin-testtools-6.7.0.jar okhttp-4.12.0.jar okio-3.6.0.jar oki
o-jvm-3.6.0.jar kotlin-stdlib-common-1.9.10.jar javalin-micrometer-6.7.
0.jar micrometer-core-1.14.5.jar micrometer-commons-1.14.5.jar micromet
er-observation-1.14.5.jar HdrHistogram-2.2.2.jar LatencyUtils-2.0.3.jar
micrometer-jetty11-1.14.5.jar jackson-databind-2.18.3.jar jackson-anno
tations-2.18.3.jar jackson-core-2.18.3.jar jackson-module-kotlin-2.18.3
.jar kotlin-reflect-1.8.10.jar jackson-datatype-jsr310-2.18.3.jar brotl
i4j-1.18.0.jar service-1.18.0.jar native-windows-x86_64-1.18.0.jar http
2-server-11.0.25.jar http2-common-11.0.25.jar http2-hpack-11.0.25.jar j
etty-alpn-conscrypt-server-11.0.25.jar conscrypt-openjdk-uber-2.5.2.jar
jetty-alpn-server-11.0.25.jar jetty-io-11.0.25.jar jetty-util-11.0.25.
jar logback-classic-1.5.18.jar logback-core-1.5.18.jar kotlin-stdlib-jd
k8-1.9.25.jar kotlin-stdlib-1.9.25.jar annotations-13.0.jar kotlin-stdl
ib-jdk7-1.9.25.jar

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,73 +10,379 @@ 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.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import somecodes.Codes.Companion.ValidFile import sbc.DigitalOutput
import sbc.NanopiDuo2
import sbc.SbcInfo
import sbc.cpuinfo
import sbc.meminfo
import sbc.netdev
import somecodes.Codes
import somecodes.Codes.Companion.isLinux
import web.WsCommand import web.WsCommand
import java.util.function.BiFunction import java.util.function.BiFunction
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or //TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// 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() {
// change when updating the application
val appversion = "0.1.1"
val logger = LoggerFactory.getLogger("Main") val logger = LoggerFactory.getLogger("Main")
val objectMapper = ObjectMapper() val objectMapper = jacksonObjectMapper()
logger.info("Application started, version $appversion")
val cfg = configFile() val cfg = configFile()
cfg.Load() cfg.Load()
val au = AudioUtility()
/**
* Coroutine scope for the application
*/
val appscope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
/**
* various SBC related functions and information
*/
var sbc : SbcInfo?
/**
* cpu information, one time read
*/
var cpuinfo: cpuinfo?
/**
* ram information, will be updated periodically in appscope
*/
var raminfo: meminfo?
/**
* cpu statistics, will be updated periodically in appscope
* key is cpu name
* value is cpu utilization percentage
* e.g. cpustat["cpu0"] = 12.5
*/
val cpustat = HashMap<String, Double>()
/**
* network statistics, will be updated periodically in appscope
* key is network interface name
* value is a Pair of strings, first is RX transfer speed per second, second is TX transfer speed per second
*/
val networkstat = HashMap<String, Pair<String, String>>()
if (isLinux()){
sbc = SbcInfo()
cpuinfo = sbc.getCpuInfo()
//logger.info("CPU Info: ${cpuinfo?.hardware?:"N/A"}, Cores: ${cpuinfo?.core?:"N/A"}, SerialNumber: ${cpuinfo?.serialNumber?:"N/A"}")
appscope.launch {
// run while the application is active
while(isActive){
raminfo = sbc.getMemInfo()
//logger.info(raminfo?.toString() ?: "Failed to get RAM info")
sbc.getCpuUtilization { it ->
// result will come 1 second later, as array of pairs,
// with first being cpu name and second being utilization percentage
it.forEach { cpuinfo ->
cpustat[cpuinfo.first] = cpuinfo.second
}
//logger.info("CPU Utilization: ${cpustat.map { "${it.key.ifBlank { "avg" }}: ${ it.value}%" }}")
}
sbc.getNetworkTransferSpeed { it ->
// result will come 1 second later, as map of network interface name to Pair of RX and TX speeds
it.forEach { xx ->
networkstat[xx.key] = Pair(
netdev.toText(xx.value.first)+ "/s",
netdev.toText(xx.value.second) + "/s"
)
}
//logger.info("Network Transfer Speed: ${networkstat.map { "${it.key}: RX=${it.value.key}, TX=${it.value.value}" }}")
}
delay(5000) // Update every 5 seconds
}
}
}
var relay1 : DigitalOutput? = null
var relay2 : DigitalOutput? = null
var commandLED : DigitalOutput? = null
var streamingLED : DigitalOutput? = null
appscope.launch {
relay1 = DigitalOutput(NanopiDuo2.CLK.linuxGpio, "Relay1", true)
relay2 = DigitalOutput(NanopiDuo2.MISO.linuxGpio, "Relay2", true)
commandLED = DigitalOutput(NanopiDuo2.TX1.linuxGpio, "CommandLED", true)
streamingLED = DigitalOutput(NanopiDuo2.RX1.linuxGpio, "StreamingLED", true)
relay1.setOFF()
relay2.setOFF()
commandLED.setOFF()
streamingLED.setOFF()
}
var audioID = 0 var audioID = 0
val preferedAudioDevice = "Speakers" val preferedAudioDevice = "USB Audio"
au.DetectPlaybackDevices().forEach { pair -> AudioUtility.LoadLibraries()
println("Device ID: ${pair.first}, Name: ${pair.second}") AudioUtility.PrintVersion()
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) audioID = 1 // fallback to first device if preferred not found
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.")
relay1?.setOFF()
relay2?.setOFF()
}
override fun onDisconnected(reason: String) {
logger.info("Disconnected from Zello Server, reason: $reason")
logger.info("Reconnecting after 10 seconds...")
z.Stop()
relay1?.setOFF()
relay2?.setOFF()
val e = this
appscope.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
commandLED?.Blink()
if (o.Start()){
relay1?.setON()
relay2?.setON()
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()
relay1?.setON()
relay2?.setOFF()
commandLED?.Blink()
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)
streamingLED?.Blink()
}
}
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") commandLED?.Blink()
WsReply(cmd.command,objectMapper.writeValueAsString(cfg) ) val data = mapOf(
"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" -> {
commandLED?.Blink()
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}")
} }
} }
"setMessageConfig"-> { "setMessageConfig"-> {
commandLED?.Blink()
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}")
} }
@@ -87,6 +393,7 @@ fun main() {
"prerecordedbroadcast" -> when(cmd.command){ "prerecordedbroadcast" -> when(cmd.command){
"getMessageConfig" ->{ "getMessageConfig" ->{
commandLED?.Blink()
val data = mapOf( val data = mapOf(
"M1" to cfg.M1, "M1" to cfg.M1,
"M2" to cfg.M2, "M2" to cfg.M2,
@@ -100,13 +407,20 @@ fun main() {
WsReply(cmd.command, objectMapper.writeValueAsString(data)) WsReply(cmd.command, objectMapper.writeValueAsString(data))
} }
"getPlaybackStatus" ->{ "getPlaybackStatus" ->{
commandLED?.Blink()
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" ->{
commandLED?.Blink()
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,29 +434,55 @@ fun main() {
null null
} }
} }
if (ValidFile(filename)){ relay1?.setOFF()
relay2?.setOFF()
if (filename!=null){
try{ try{
val player= AudioFilePlayer(audioID, filename) afp= AudioFilePlayer(audioID, filename)
player.Play { _ -> afp = null} afp?.Play { _ ->
afp = player afp = null
relay1?.setOFF()
relay2?.setOFF()
}
relay1?.setON()
relay2?.setON()
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" ->{
commandLED?.Blink()
afp?.Stop() afp?.Stop()
afp = null afp = null
relay1?.setOFF()
relay2?.setOFF()
WsReply(cmd.command,"success") WsReply(cmd.command,"success")
} }
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" -> {
commandLED?.Blink()
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 +491,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

@@ -1,74 +1,73 @@
package audio package audio
import audio.AudioUtility.Companion.bass
import kotlinx.coroutines.CoroutineScope 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
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()}") AudioUtility.setVolumeOutput(deviceID,1.0f)
} filehandle = bass.BASS_StreamCreateFile(false, fullpath.absolutePathString(), 0, 0, 0)
} else throw Exception("Failed to set device $deviceID") if (filehandle!=0){
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)){
bass.BASS_ChannelSetAttribute(filehandle, Bass.BASS_ATTRIB_VOL, 1.0f)
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

@@ -1,13 +1,62 @@
package audio package audio
import com.sun.jna.Platform
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.isRegularFile
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
@Suppress("unused")
class AudioUtility { class AudioUtility {
private val bass = Bass.Instance companion object{
val bass : Bass = Bass.Instance
private val logger : Logger = LoggerFactory.getLogger(AudioUtility::class.java) private val logger : Logger = LoggerFactory.getLogger(AudioUtility::class.java)
init{ fun ExtractLibraries(parent: String, filename: String, targetPath: Path) : Path?{
logger.info("Bass Version = ${bass.BASS_GetVersion().toHexString()}") if (targetPath.resolve(filename).isRegularFile()) return targetPath.resolve(filename) // already exists
val out = targetPath.resolve(filename)
try{
AudioUtility::class.java.getResourceAsStream("$parent/$filename").use { ins ->
requireNotNull(ins) { "Resource $parent/$filename not found" }
Files.copy(ins, out, REPLACE_EXISTING)
}
out.toFile().setReadable(true, false)
out.toFile().setExecutable(true, false)
return out
} catch (_: Exception){
return null
}
}
fun LoadLibraries(){
val runfolder = Path.of(System.getProperty("user.dir"))
//logger.info("Checking for BASS libraries in $runfolder")
val resourceprefix = Platform.RESOURCE_PREFIX
logger.info("Resource prefix : $resourceprefix")
val bass = ExtractLibraries(resourceprefix, System.mapLibraryName("bass"), runfolder)
val bassopus = ExtractLibraries(resourceprefix, System.mapLibraryName("bassopus"), runfolder)
if (bass!=null){
System.load(bass.toString())
}
if (bassopus!=null){
System.load(bassopus.toString())
}
}
fun PrintVersion(){
logger.info("BASS Version: ${bass.BASS_GetVersion().toHexString()}")
}
fun LoadPlugin(plugin: String){
if (bass.BASS_PluginLoad(plugin, 0)>0){
logger.info("Plugin $plugin loaded successfully")
} else {
logger.error("Failed to load plugin $plugin: ${bass.BASS_ErrorGetCode()}")
}
} }
fun DetectPlaybackDevices() : List<Pair<Int, String>> { fun DetectPlaybackDevices() : List<Pair<Int, String>> {
@@ -23,6 +72,14 @@ class AudioUtility {
return result return result
} }
fun setVolumeOutput(deviceID: Int, value: Float){
var vol = value
if (vol < 0) vol = 0f
if (vol > 1) vol = 1f
bass.BASS_SetDevice(deviceID)
bass.BASS_SetVolume(vol)
}
fun InitDevice(deviceID: Int, device_samplingrate: Int = 48000) : Boolean { fun InitDevice(deviceID: Int, device_samplingrate: Int = 48000) : Boolean {
val dev = Bass.BASS_DEVICEINFO() val dev = Bass.BASS_DEVICEINFO()
if (bass.BASS_GetDeviceInfo(deviceID, dev)) { if (bass.BASS_GetDeviceInfo(deviceID, dev)) {
@@ -38,4 +95,23 @@ 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()
}
}
} }

View File

@@ -8,6 +8,7 @@ import com.sun.jna.*;
public interface Bass extends Library { public interface Bass extends Library {
Bass Instance = Native.load("bass", Bass.class); Bass Instance = Native.load("bass", Bass.class);
int BASSVERSION = 0x204; // API version int BASSVERSION = 0x204; // API version
String BASSVERSIONTEXT = "2.4"; String BASSVERSIONTEXT = "2.4";

View File

@@ -1,5 +1,6 @@
package audio package audio
import audio.AudioUtility.Companion.bass
import com.sun.jna.Memory import com.sun.jna.Memory
import com.sun.jna.Pointer import com.sun.jna.Pointer
import org.slf4j.Logger import org.slf4j.Logger
@@ -12,37 +13,40 @@ 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 bassopus = BASSOPUS.Instance
private var filehandle = 0 private var filehandle = 0
private val logger : Logger = LoggerFactory.getLogger(OpusStreamReceiver::class.java) private val logger : Logger = LoggerFactory.getLogger(OpusStreamReceiver::class.java)
var isPlaying = false var isPlaying = false
val bassopus : BASSOPUS = BASSOPUS.Instance
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
opushead.inputrate = samplingrate opushead.inputrate = samplingrate
val procpush = Pointer(-1) val procpush = Pointer(-1)
AudioUtility.setVolumeOutput(deviceID, 1.0f)
filehandle = bassopus.BASS_OPUS_StreamCreate(opushead,0, procpush, null) filehandle = bassopus.BASS_OPUS_StreamCreate(opushead,0, procpush, null)
if (filehandle != 0){ if (filehandle != 0){
if (bass.BASS_ChannelPlay(filehandle,false)){ if (bass.BASS_ChannelPlay(filehandle,false)){
bass.BASS_ChannelSetAttribute(filehandle, Bass.BASS_ATTRIB_VOL, 1.0f)
isPlaying = true isPlaying = true
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 +55,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()
} }
/** /**

157
src/sbc/DigitalOutput.kt Normal file
View File

@@ -0,0 +1,157 @@
package sbc
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.slf4j.LoggerFactory
import somecodes.Codes.Companion.gpioExportPath
import somecodes.Codes.Companion.gpioPath
import somecodes.Codes.Companion.gpioUnexportPath
import somecodes.Codes.Companion.haveGpioSupport
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.isDirectory
/**
* Create a digital output for a GPIO pin.
* @param linuxGpio The GPIO pin number in Linux.
* @param name The name of the digital output.
* @param activeHigh If true, the output is active high (default is true).
* @property inited Indicates whether the digital output has been initialized.
*
*/
@Suppress("unused")
class DigitalOutput(val linuxGpio: Int, val name: String, val activeHigh: Boolean = true) {
var inited = false; private set
private var valuepath: Path? = null
private val logger = LoggerFactory.getLogger("DigitalOutput $name ($linuxGpio)")
init{
if (haveGpioSupport()){
if (checkExists()){
// already exists
try{
// try to set direction to output
val dir = gpioPath.resolve("gpio$linuxGpio").resolve("direction")
Files.writeString(dir, "out")
// successfully set direction to output
inited = true
valuepath = gpioPath.resolve("gpio$linuxGpio").resolve("value")
logger.info("GPIO $name GPIO $linuxGpio already exists as output")
} catch (e : Exception){
// failed to set direction to output, log error
logger.error("Failed to set existing GPIO $name GPIO $linuxGpio as output: ${e.message}")
}
} else {
// not yet exists, export it
try{
// export the GPIO pin
Files.writeString(gpioExportPath, linuxGpio.toString())
if (checkExists()){
// successfully exported, now set direction to output
val dir = gpioPath.resolve("gpio$linuxGpio").resolve("direction")
Files.writeString(dir, "out")
inited = true
valuepath = gpioPath.resolve("gpio$linuxGpio").resolve("value")
logger.info("Initialized GPIO $name GPIO $linuxGpio as output")
} else {
// failed to export, log error
logger.error("GPIO $name GPIO $linuxGpio does not exist after export")
}
} catch (e: Exception){
// failed to export GPIO pin, log error
logger.error("Failed to export GPIO $name GPIO $linuxGpio: ${e.message}")
}
}
} else logger.error("DigitalOutput $name GPIO $linuxGpio not initialized: GPIO support not available")
}
fun checkExists(): Boolean{
return try {
val gpioPath = gpioPath.resolve("gpio$linuxGpio")
if (gpioPath.isDirectory() && gpioPath.toFile().exists()){
val dir = gpioPath.resolve("direction").toFile()
val value = gpioPath.resolve("value").toFile()
if (dir.exists() && value.exists()) {
return dir.canWrite() && value.canWrite()
}
}
false
} catch (e: Exception) {
logger.error("Error checking if GPIO $linuxGpio exists: ${e.message}")
false
}
}
/**
* Release the GPIO pin by unexporting it.
*/
fun release(){
if (inited){
try{
Files.writeString(gpioUnexportPath, linuxGpio.toString())
inited = false
valuepath = null
} catch (e : Exception){
logger.error("Failed to unexport GPIO $name GPIO ${e.message}")
}
}
}
/**
* Set Digital Output to ON
*/
fun setON() : Boolean{
if (inited){
if (valuepath!=null){
try{
Files.writeString(valuepath!!, if (activeHigh) "1" else "0")
return true
} catch (e: Exception){
logger.error("Failed to set GPIO $name GPIO $linuxGpio ON: ${e.message}")
}
}
}
return false
}
/**
* Set Digital Output to OFF
*/
fun setOFF() : Boolean{
if (inited){
if (valuepath!=null){
try{
Files.writeString(valuepath!!, if (activeHigh) "0" else "1")
return true
} catch (e : Exception){
logger.error("Failed to set GPIO $name GPIO $linuxGpio OFF: ${e.message}")
}
}
}
return false
}
/**
* Blink the Digital Output for a specified duration.
* @param ms The duration in milliseconds to keep the output ON before turning it OFF (default is 50ms).
*/
fun Blink(ms: Long = 50){
if (inited){
CoroutineScope(Dispatchers.IO).launch {
setON()
delay(ms)
setOFF()
}
}
}
}

21
src/sbc/NanopiDuo2.kt Normal file
View File

@@ -0,0 +1,21 @@
package sbc
/**
* Nanopi Duo2 pin and GPIO mapping.
* Source : https://wiki.friendlyelec.com/wiki/index.php/NanoPi_Duo2
*/
@Suppress("unused")
enum class NanopiDuo2(val pin: Int, val linuxGpio: Int) {
IRRX(9,363),
PG11(11,203),
RXD(2,5),
TXD(4,4),
SCL(8,11),
SDA(10,12),
CS(12,13),
CLK(14,14),
MISO(16,16),
MOSI(18,15),
RX1(20,199),
TX1(22,198)
}

39
src/sbc/RaspberryPi4.kt Normal file
View File

@@ -0,0 +1,39 @@
package sbc
/**
* Raspberry Pi 4 pin and GPIO mapping.
* Source : https://pinout.xyz/#
*/
@Suppress("unused")
enum class RaspberryPi4(val pin: Int, val linuxGpio: Int, val alias:String? = null) {
GPIO2(3, 2, "SDA1"),
GPIO3(5, 3, "SCL1"),
GPIO4(7, 4, "GPCLK0"),
GPIO17(11, 17),
GPIO27(13, 27),
GPIO10(19, 10, "MOSI"),
GPIO9(21, 9, "MISO"),
GPIO11(23, 11, "SCLK"),
GPIO0(27, 0,"EEPROM SDA"),
GPIO5(29, 5),
GPIO6(31, 6),
GPIO13(33, 13,"PWM1"),
GPIO19(35, 19, "PCM FS"),
GPIO26(37, 26),
GPIO14(8, 14, "TXD"),
GPIO15(10, 15, "RXD"),
GPIO18(12, 18, "PCM CLK"),
GPIO23(16, 23),
GPIO24(18, 24),
GPIO25(22, 25),
GPIO8(24, 8,"SPI CE0"),
GPIO7(26, 7, "SPI CE1"),
GPIO1(28, 1, "EEPROM SCL"),
GPIO12(32, 12, "PWM0"),
GPIO16(36, 16),
GPIO20(38, 20,"PCM DIN"),
GPIO21(40, 21, "PCM DOUT"),;
}

300
src/sbc/SbcInfo.kt Normal file
View File

@@ -0,0 +1,300 @@
package sbc
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.slf4j.LoggerFactory
import somecodes.Codes.Companion.isLinux
import java.io.File
import java.util.function.Consumer
import kotlin.io.path.Path
import kotlin.io.path.exists
import kotlin.io.path.readText
/**
* get SBC Information
*/
@Suppress("unused")
class SbcInfo {
private val logger = LoggerFactory.getLogger("SbcInfo")
/**
* Get CPU information from /proc/cpuinfo on Linux systems.
* @return cpuinfo data class containing core count, hardware model, and serial number.
* Returns null if the platform is not Linux or if an error occurs while reading the file
*/
fun getCpuInfo(): cpuinfo? {
return try {
if (isLinux()) {
val lines = File("/proc/cpuinfo").readLines()
if (lines.isNotEmpty()) {
val core = lines.count { it.startsWith("processor") }
val hardware =
lines.firstOrNull { it.startsWith("Hardware") }?.substringAfter(":")?.trim() ?: "Unknown"
val serialNumber =
lines.firstOrNull { it.startsWith("Serial") }?.substringAfter(":")?.trim() ?: "Unknown"
cpuinfo(core, hardware, serialNumber)
} else {
logger.error("No CPU info found in /proc/cpuinfo")
null
}
} else {
logger.error("Platform is not Linux, cannot get CPU info")
null
}
} catch (e: Exception) {
logger.error("Error getting CPU info: ${e.message}")
null
}
}
/**
* Get memory information from /proc/meminfo on Linux systems.
* @return meminfo data class containing total, free, available memory, buffers, cached memory, and swap info.
* Returns null if the platform is not Linux or if an error occurs while reading the file
*/
fun getMemInfo(): meminfo? {
return try {
if (isLinux()) {
val memInfoFile = File("/proc/meminfo")
if (memInfoFile.exists()) {
val lines = memInfoFile.readLines()
val totalMem =
lines.firstOrNull { it.startsWith("MemTotal:") }?.substringAfter(":")?.trim()?.split(" ")
?.first()?.toLongOrNull() ?: 0L
val freeMem =
lines.firstOrNull { it.startsWith("MemFree:") }?.substringAfter(":")?.trim()?.split(" ")
?.first()?.toLongOrNull() ?: 0L
val availableMem =
lines.firstOrNull { it.startsWith("MemAvailable:") }?.substringAfter(":")?.trim()?.split(" ")
?.first()?.toLongOrNull() ?: 0L
val buffers =
lines.firstOrNull { it.startsWith("Buffers:") }?.substringAfter(":")?.trim()?.split(" ")
?.first()?.toLongOrNull() ?: 0L
val cached =
lines.firstOrNull { it.startsWith("Cached:") }?.substringAfter(":")?.trim()?.split(" ")?.first()
?.toLongOrNull() ?: 0L
val swapTotal =
lines.firstOrNull { it.startsWith("SwapTotal:") }?.substringAfter(":")?.trim()?.split(" ")
?.first()?.toLongOrNull() ?: 0L
val swapFree = lines.firstOrNull { it.startsWith("SwapFree:") }
meminfo(
totalMem,
freeMem,
availableMem,
buffers,
cached,
swapTotal,
swapFree?.substringAfter(":")?.trim()?.split(" ")?.first()?.toLongOrNull() ?: 0L
)
} else {
logger.error("/proc/meminfo does not exist")
null
}
} else {
logger.error("Platform is not Linux, cannot get memory info")
null
}
} catch (e: Exception) {
logger.error("Error getting memory info: ${e.message}")
null
}
}
/**
* Get CPU utilization percentage for each CPU core.
* @param percent Consumer that accepts an array of pairs containing CPU core names and their utilization percentages.
* This function runs in a coroutine and retrieves CPU usage from /proc/stat on Linux systems.
*/
fun getCpuUtilization(percent: Consumer<Array<Pair<String, Double>>>) {
if (isLinux()) {
CoroutineScope(Dispatchers.Default).launch {
val map1 = HashMap<String, cpustat>()
val map2 = HashMap<String, cpustat>()
val s1 = File("/proc/stat").readLines().filter { it.startsWith("cpu") }
s1.forEach {
val match = cpustat.regex.find(it)
if (match != null) {
val parts = match.groupValues
if (parts.size >= 11) {
val first = cpustat(
user = parts[2].toLong(),
nice = parts[3].toLong(),
system = parts[4].toLong(),
idle = parts[5].toLong(),
iowait = parts[6].toLong(),
irq = parts[7].toLong(),
softirq = parts[8].toLong(),
steal = parts[9].toLong(),
guest = parts[10].toLongOrNull() ?: 0L,
guestNice = parts.getOrNull(11)?.toLongOrNull() ?: 0L
)
map1[parts[1].trim()] = first
}
}
}
delay(1000) // Wait for 1 second to get a new snapshot
val s2 = File("/proc/stat").readLines().filter { it.startsWith("cpu") }
s2.forEach {
val match = cpustat.regex.find(it)
if (match != null) {
val parts = match.groupValues
if (parts.size >= 11) {
val second = cpustat(
user = parts[2].toLong(),
nice = parts[3].toLong(),
system = parts[4].toLong(),
idle = parts[5].toLong(),
iowait = parts[6].toLong(),
irq = parts[7].toLong(),
softirq = parts[8].toLong(),
steal = parts[9].toLong(),
guest = parts[10].toLongOrNull() ?: 0L,
guestNice = parts.getOrNull(11)?.toLongOrNull() ?: 0L
)
map2[parts[1].trim()] = second
}
}
}
// compare s1 and s2
val result = ArrayList<Pair<String, Double>>()
map1.forEach {
val key = it.key
val firstStat = it.value
val secondStat = map2[key]
if (secondStat != null) {
val usage = secondStat.cpuUsage(firstStat)
result.add(Pair(key, usage))
}
}
percent.accept(result.toTypedArray())
}
} else {
logger.error("Platform is not Linux, cannot get CPU utilization")
percent.accept(arrayOf(Pair("N/A", 0.0)))
}
}
/**
* Get network status from /proc/net/dev on Linux systems.
* @return List of netdev data class containing network device name, RX and TX statistics.
* Returns null if the platform is not Linux or if an error occurs while reading the file
*/
fun getNetworkStatus(): List<netdev>? {
return try {
if (isLinux()) {
val netdevFile = File("/proc/net/dev")
if (netdevFile.exists()) {
val lines = netdevFile.readLines().drop(2) // Skip the first two header lines
val regex = netdev.regex
val devices = lines.mapNotNull { line ->
val match = regex.find(line)
if (match != null) {
val parts = match.groupValues
val name = parts[1].trim().removeSuffix(":")
val rx = devstat(
Bytes = parts[2].toLong(),
Packets = parts[3].toLong(),
Errors = parts[4].toLong(),
Drops = parts[5].toLong(),
Fifo = parts[6].toLong(),
Frame = parts[7].toLong(),
Compressed = parts[8].toLong(),
Multicast = parts[9].toLong()
)
val tx = devstat(
Bytes = parts[10].toLong(),
Packets = parts[11].toLong(),
Errors = parts[12].toLong(),
Drops = parts[13].toLong(),
Fifo = parts[14].toLong(),
Frame = parts[15].toLong(),
Compressed = parts[16].toLong(),
Multicast = parts[17].toLong()
)
netdev(name, rx, tx)
} else {
null
}
}
devices
} else {
logger.error("/proc/net/dev does not exist")
null
}
} else {
logger.error("Platform is not Linux, cannot get network status")
null
}
} catch (e: Exception) {
logger.error("Error getting network status: ${e.message}")
null
}
}
/**
* Get network transfer speed for each network device.
* @param callback Consumer that accepts a map where keys are network device names and values are
* pairs of RX and TX bytes difference within a 1-second interval.
* This function runs in a coroutine and retrieves network statistics from /proc/net/dev on Linux
*/
fun getNetworkTransferSpeed(callback: Consumer<Map<String, Pair<Long, Long>>>) {
if (isLinux()) {
CoroutineScope(Dispatchers.Default).launch {
val netdevs = getNetworkStatus()
if (netdevs != null) {
val speedMap = netdevs.associate {
it.name to Pair(it.RX.Bytes, it.TX.Bytes)
}
delay(1000) // Wait for 1 second to get a new snapshot
val newNetdevs = getNetworkStatus()
if (newNetdevs != null) {
val newSpeedMap = newNetdevs.associate {
it.name to Pair(it.RX.Bytes, it.TX.Bytes)
}
val resultMap = mutableMapOf<String, Pair<Long, Long>>()
speedMap.forEach { (name, oldSpeed) ->
val newSpeed = newSpeedMap[name]
if (newSpeed != null) {
val rxDiff = newSpeed.first - oldSpeed.first
val txDiff = newSpeed.second - oldSpeed.second
resultMap[name] = Pair(rxDiff, txDiff)
}
}
callback.accept(resultMap)
} else callback.accept(emptyMap())
}
}
} else {
logger.error("Platform is not Linux, cannot get network transfer speed")
callback.accept(emptyMap())
}
}
/**
* Get CPU Thermal information from /sys/class/thermal/thermal_zone0/temp on Linux systems.
* @return CPU temperature in degrees Celsius. Returns 0.0 if the platform is not Linux or if an error occurs while reading the file.
*/
fun getCPUThermal(): Double {
return try{
if (isLinux()){
val p = Path("/sys/class/thermal/thermal_zone0/temp")
if (p.exists()) {
val vv = p.readText().trim().toDouble()
val tempCelsius = vv / 1000.0 // Convert from millidegrees Celsius to degrees Celsius
(tempCelsius*10).toInt() / 10.0 // Round to 1 decimal places
} else throw Exception("/sys/class/thermal/thermal_zone0/temp does not exist")
} else throw Exception("Platform is not Linux, cannot get CPU thermal info")
} catch (_ : Exception) {
0.0
}
}
}

3
src/sbc/cpuinfo.kt Normal file
View File

@@ -0,0 +1,3 @@
package sbc
data class cpuinfo(val core: Int, val hardware: String, val serialNumber: String)

49
src/sbc/cpustat.kt Normal file
View File

@@ -0,0 +1,49 @@
package sbc
/**
* Data class representing CPU statistics.
* @property user Time spent in user mode.
* @property nice Time spent in user mode with low priority (nice).
* @property system Time spent in system mode.
* @property idle Time spent in idle mode.
* @property iowait Time spent waiting for I/O operations to complete.
* @property irq Time spent servicing hardware interrupts.
* @property softirq Time spent servicing software interrupts.
* @property steal Time spent in involuntary wait by virtual CPUs.
* @property guest Time spent running a virtual CPU for guest operating systems.
* @property guestNice Time spent running a virtual CPU for guest operating systems with low priority (nice).
*/
@Suppress("unused")
class cpustat(val user: Long, val nice: Long, val system: Long, val idle: Long, val iowait: Long, val irq: Long, val softirq: Long, val steal: Long, val guest: Long, val guestNice: Long) {
companion object{
val regex = Regex("""cpu(\d?)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)""")
}
override fun toString(): String {
return "CPU Stat(user=$user, nice=$nice, system=$system, idle=$idle, iowait=$iowait, irq=$irq, softirq=$softirq)"
}
fun total(): Long {
return user + nice + system + idle + iowait + irq + softirq + steal + guest + guestNice
}
fun idle(): Long {
return idle + iowait
}
/**
* Calculate CPU usage percentage based on the current and previous cpustat instances.
* @param previous The previous cpustat instance to compare against.
* @return CPU usage percentage as a Double, rounded to one decimal place.
*/
fun cpuUsage(previous: cpustat): Double {
val totalDiff = total() - previous.total()
if (totalDiff < 0L) return 0.0 // Avoid division by zero
val idleDiff = idle() - previous.idle()
if (idleDiff < 0L) return 0.0 // Avoid negative idle difference
return (1.0*(totalDiff - idleDiff) / totalDiff * 100.0).let {
val x = (it * 10).toInt()
x/10.0
}
}
}

5
src/sbc/devstat.kt Normal file
View File

@@ -0,0 +1,5 @@
package sbc
data class devstat(val Bytes: Long, val Packets: Long, val Errors: Long, val Drops: Long, val Fifo: Long, val Frame: Long, val Compressed: Long, val Multicast: Long)

76
src/sbc/meminfo.kt Normal file
View File

@@ -0,0 +1,76 @@
package sbc
/**
* Memory Information data class from /proc/meminfo on Linux systems.
* all values are in kilobytes (KB).
* @property MemTotal Total usable RAM (i.e., physical RAM minus a few reserved bits and the kernel binary code).
* @property MemFree The amount of physical RAM, in kilobytes, left unused by the system.
* @property MemAvailable An estimate of how much memory is available for starting new applications, without swapping.
* @property Buffers The amount of memory, in kilobytes, used by kernel buffers.
* @property Cached The amount of memory, in kilobytes, used by the page cache and slabs.
* @property SwapTotal Total amount of swap space available, in kilobytes.
* @property SwapFree The amount of swap space that is currently unused, in kilobytes.
*/
@Suppress("unused")
class meminfo(val MemTotal: Long, val MemFree: Long, val MemAvailable: Long, val Buffers: Long, val Cached: Long, val SwapTotal: Long, val SwapFree: Long) {
companion object{
private const val MB_Threshold = 1024.0
private const val GB_Threshold = 1024.0 * MB_Threshold
/**
* Converts a memory value in kilobytes to a human-readable string.
* @param value The memory value in kilobytes.
* @return A string representation of the memory value in GB, MB, or KB, and round to two decimal places.
* For example, "1.50 GB", "512 MB", or "256 KB
*/
fun toText(value: Long) : String {
return when {
value >= 1024 * 1024 -> "${"%.2f".format(value / GB_Threshold)} GB"
value >= 1024 -> "${"%.2f".format(value / MB_Threshold)} MB"
else -> "$value KB"
}
}
}
override fun toString(): String {
return "MemTotal=${toText(MemTotal)}, MemFree=${toText(MemFree)}, MemAvailable=${toText(MemAvailable)}, Buffers=${toText(Buffers)}, Cached=${toText(Cached)}, SwapTotal=${toText(SwapTotal)}, SwapFree=${toText(SwapFree)}, "
}
/**
* Calculates the used memory in kilobytes.
* @return The amount of used memory in kilobytes.
*/
fun MemUsed(): Long {
return MemTotal - MemFree
}
/**
* Calculates the percentage of memory used.
* @return The percentage of memory used, as a double, rounded to two decimal places.
*/
fun percentUsed(): Double {
return if (MemTotal > 0) {
((1.0 * MemUsed() / MemTotal) * 100.0).let {
val x = (it * 100).toInt()
x / 100.0
}
} else {
0.0
}
}
/**
* Calculates the percentage of memory available.
* @return The percentage of memory available, as a double, rounded to two decimal places.
*/
fun percentFree(): Double {
return if (MemTotal > 0) {
((1.0 * MemFree / MemTotal) * 100.0).let {
val x = (it * 100).toInt()
x/ 100.0
}
} else {
0.0
}
}
}

28
src/sbc/netdev.kt Normal file
View File

@@ -0,0 +1,28 @@
package sbc
class netdev( val name: String,val RX: devstat, val TX: devstat) {
companion object{
val regex = Regex("""\s?(\S+):\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)""")
private const val KB_Size = 1024.0
private const val MB_Size = KB_Size * 1024.0
private const val GB_Size = MB_Size * 1024.0
/**
* Converts a long value to a human-readable text format.
* @param value The long value to convert.
* @return A string representation of the value, formatted with appropriate units.
* The value is formatted in bytes, kilobytes, megabytes, or gigabytes, and rounded to one decimal place.
*/
fun toText(value: Long): String {
return when {
value >= 1024 * 1024 * 1024 -> "${"%.1f".format(value / (GB_Size))} GB"
value >= 1024 * 1024 -> "${"%.1f".format(value / (MB_Size))} MB"
value >= 1024 -> "${"%.1f".format(value / KB_Size)} KB"
else -> "$value B"
}
}
}
}

View File

@@ -1,12 +1,64 @@
package somecodes package somecodes
import com.fasterxml.jackson.databind.ObjectMapper import com.sun.jna.Platform
import org.slf4j.LoggerFactory
import java.io.File import java.io.File
import java.nio.file.Path
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")
val gpioPath : Path = Path("/sys/class/gpio")
val gpioExportPath : Path = gpioPath.resolve("export")
val gpioUnexportPath : Path = gpioPath.resolve("unexport")
private val logger = LoggerFactory.getLogger("Codes")
private val validAudioExtensions = setOf("wav", "mp3")
private const val KB_size = 1024.0 // 1 KB = 1024 bytes
private const val MB_size = 1024.0 * KB_size // 1 MB = 1024 KB
private const val GB_size = 1024.0 * MB_size // 1 GB = 1024 MB
fun isLinux() : Boolean {
return Platform.isLinux()
}
fun haveGpioSupport() : Boolean {
if (Platform.isLinux()){
if (gpioExportPath.toFile().exists() && gpioUnexportPath.toFile().exists()) {
if (gpioExportPath.toFile().canWrite() && gpioUnexportPath.toFile().canWrite()) {
return true
} else logger.error("$gpioExportPath or $gpioUnexportPath is not writable")
} else logger.error("$gpioExportPath or $gpioUnexportPath does not exist")
} else logger.error("Platform is not Linux, GPIO support is not available")
return false
}
fun SizeToString(size: Long) : String {
return when {
size >= GB_size -> String.format("%.2f GB", size / GB_size)
size >= MB_size -> String.format("%.2f MB", size / MB_size)
size >= KB_size -> String.format("%.2f KB", size / 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;
@@ -30,7 +35,7 @@ public class webApp {
} }
app = Javalin.create(config -> { app = Javalin.create(config -> {
config.useVirtualThreads = true; // Enable virtual threads for better performance config.useVirtualThreads = true; // Enable virtual threads for better performance
config.staticFiles.add("/"); config.staticFiles.add("/webpage");
config.router.apiBuilder(()->{ config.router.apiBuilder(()->{
path("/", () -> get(ctx -> { path("/", () -> get(ctx -> {
if (ctx.sessionAttribute("user") == null) { if (ctx.sessionAttribute("user") == null) {
@@ -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() ->{
@@ -119,7 +128,7 @@ class ZelloClient(val address : URI, val username: String, val password: String)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun onMessage(message: String?) { override fun onMessage(message: String?) {
logger.info("Message received: $message") //logger.info("Message received: $message")
val jsnode = mapper.readTree(message) val jsnode = mapper.readTree(message)
if (jsnode["seq"] != null) { if (jsnode["seq"] != null) {
when(val seq = jsnode.get("seq").asInt()){ when(val seq = jsnode.get("seq").asInt()){
@@ -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,25 +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
// val thread = Thread {
// try {
// Thread.sleep(10000)
// connect()
// } catch (e: InterruptedException) {
// logger.error("Reconnection interrupted: ${e.message}")
// }
// }
// thread.name= "ZelloClient-ReconnectThread"
// thread.isDaemon = true
// thread.start()
} }
override fun onError(ex: Exception?) { override fun onError(ex: Exception?) {

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)