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 ###
out/
audiofile/
!**/src/main/**/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="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="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="FunctionName" enabled="false" level="WEAK WARNING" 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$/testResources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/html" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/audiofile" />
</content>
<orderEntry type="inheritedJdk" />
<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() {
// Your code here
console.log('pocreceiver.js is ready!');
//console.log('pocreceiver.js is ready!');
const path = window.location.pathname;
const ws = new WebSocket('ws://' + window.location.host + path + '/ws');
ws.onopen = function() {
console.log('WebSocket connection opened');
//console.log('WebSocket connection opened');
$('#indicatorDisconnected').addClass('visually-hidden');
$('#indicatorConnected').removeClass('visually-hidden');
setInterval(function() {
sendCommand({ command: "getZelloStatus" });
}, 5000);
}, 1000);
};
ws.onmessage = function(event) {
console.log('WebSocket message received:', event.data);
//console.log('WebSocket message received:', event.data);
let msg = {};
try {
msg = JSON.parse(event.data);
@@ -25,13 +25,13 @@ $(document).ready(function() {
}
if (msg.reply === "getZelloStatus" && msg.data !== undefined && msg.data.length > 0) {
const zelloData = msg.data;
console.log('Zello Status Data:', zelloData);
$('#zelloStatus').text(zelloData.status);
//console.log('Zello Status Data:', zelloData);
$('#zelloStatus').text(zelloData);
}
};
ws.onclose = function() {
console.log('WebSocket connection closed');
//console.log('WebSocket connection closed');
$('#indicatorDisconnected').removeClass('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" 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="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;">
<!--! 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>
@@ -35,7 +36,7 @@
<h1 class="text-center">Zello Status</h1>
</div>
<div class="row">
<p id="zelloStatus">Paragraph</p>
<p class="d-flex justify-content-center" id="zelloStatus">No Status</p>
</div>
</div>
<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 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="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;">
<!--! 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>
@@ -43,7 +44,7 @@
<div class="card">
<div class="card-body" id="cardM1">
<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="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>
@@ -55,7 +56,7 @@
<div class="card">
<div class="card-body" id="cardM2">
<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="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>
@@ -69,7 +70,7 @@
<div class="card">
<div class="card-body" id="cardM3">
<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="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>
@@ -81,7 +82,7 @@
<div class="card">
<div class="card-body" id="cardM4">
<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="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>
@@ -95,7 +96,7 @@
<div class="card">
<div class="card-body" id="cardM5">
<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="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>
@@ -107,7 +108,7 @@
<div class="card">
<div class="card-body" id="cardM6">
<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="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>
@@ -121,7 +122,7 @@
<div class="card">
<div class="card-body" id="cardM7">
<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="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>
@@ -133,7 +134,7 @@
<div class="card">
<div class="card-body" id="cardM8">
<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="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>

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="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" 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;">
<!--! 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>
@@ -168,14 +169,9 @@
<div class="row w-100">
<h1 class="text-center">Content Upload</h1>
</div>
<div class="row"><input type="file" id="chosenFile"></div>
<div class="row">
<div class="col">
<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 class="row w-100">
<div class="col-10"><input type="file" id="chosenFile"></div>
<div class="col-2"><button class="btn btn-primary" id="btnUploadContent" type="button">Upload</button></div>
</div>
</div>
<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.OpusStreamReceiver
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.configFile
import web.WsReply
@@ -10,73 +10,379 @@ import web.webApp
import zello.ZelloClient
import zello.ZelloEvent
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 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 java.util.function.BiFunction
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
fun main() {
// change when updating the application
val appversion = "0.1.1"
val logger = LoggerFactory.getLogger("Main")
val objectMapper = ObjectMapper()
val objectMapper = jacksonObjectMapper()
logger.info("Application started, version $appversion")
val cfg = configFile()
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
val preferedAudioDevice = "Speakers"
au.DetectPlaybackDevices().forEach { pair ->
println("Device ID: ${pair.first}, Name: ${pair.second}")
val preferedAudioDevice = "USB Audio"
AudioUtility.LoadLibraries()
AudioUtility.PrintVersion()
AudioUtility.DetectPlaybackDevices().forEach { pair ->
logger.info("Device ID: ${pair.first}, Name: ${pair.second}")
if (pair.second.contains(preferedAudioDevice)) {
audioID = pair.first
}
}
if (audioID!=0){
val initsuccess = au.InitDevice(audioID)
println("Audio Device $audioID initialized: $initsuccess")
}
if (audioID==0) audioID = 1 // fallback to first device if preferred not found
val initsuccess = AudioUtility.InitDevice(audioID,44100)
logger.info("Audio Device $audioID initialized: $initsuccess")
// for Zello Client
val o = OpusStreamReceiver(audioID)
// for AudioFilePlayer
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 {
source: String, cmd: WsCommand ->
when (source) {
"setting" -> when(cmd.command){
"getConfig" ->{
logger.info("Get Config")
WsReply(cmd.command,objectMapper.writeValueAsString(cfg) )
commandLED?.Blink()
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" -> {
commandLED?.Blink()
try{
val xx = objectMapper.readValue(cmd.data, object: TypeReference<Map<String, String>>() {})
cfg.ZelloUsername = xx["ZelloUsername"]
cfg.ZelloPassword = xx["ZelloPassword"]
cfg.ZelloChannel = xx["ZelloChannel"]
cfg.ZelloServer = xx["ZelloServer"]
cfg.ZelloWorkNetworkName = xx["ZelloWorkNetworkName"]
cfg.ZelloEnterpriseServerDomain = xx["ZelloEnterpriseServerDomain"]
var changed = false
if (cfg.ZelloUsername != xx["ZelloUsername"]) {
cfg.ZelloUsername = xx["ZelloUsername"] ?: ""
changed = true
}
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")
} else WsReply(cmd.command,"No changes made")
} catch (e: Exception){
WsReply(cmd.command,"failed: ${e.message}")
}
}
"setMessageConfig"-> {
commandLED?.Blink()
try{
val xx = objectMapper.readValue(cmd.data, object : TypeReference<Map<String, String>>() {})
cfg.M1 = xx["M1"]
cfg.M2 = xx["M2"]
cfg.M3 = xx["M3"]
cfg.M4 = xx["M4"]
cfg.M5 = xx["M5"]
cfg.M6 = xx["M6"]
cfg.M7 = xx["M7"]
cfg.M8 = xx["M8"]
var changed = false
if (cfg.M1 != xx["M1"]) {
cfg.M1 = xx["M1"] ?: ""
changed = true
}
if (cfg.M2 != xx["M2"]) {
cfg.M2 = xx["M2"] ?: ""
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")
} else WsReply(cmd.command,"No changes made")
} catch (e: Exception){
WsReply(cmd.command,"failed: ${e.message}")
}
@@ -87,6 +393,7 @@ fun main() {
"prerecordedbroadcast" -> when(cmd.command){
"getMessageConfig" ->{
commandLED?.Blink()
val data = mapOf(
"M1" to cfg.M1,
"M2" to cfg.M2,
@@ -100,13 +407,20 @@ fun main() {
WsReply(cmd.command, objectMapper.writeValueAsString(data))
}
"getPlaybackStatus" ->{
commandLED?.Blink()
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 {
WsReply(cmd.command, "Idle")
}
}
"playMessage" ->{
commandLED?.Blink()
afp?.Stop()
afp = null
// stop Opus Receiver if it is running
o.Stop()
val filename = when(cmd.data){
"M1" -> cfg.M1
"M2" -> cfg.M2
@@ -120,29 +434,55 @@ fun main() {
null
}
}
if (ValidFile(filename)){
relay1?.setOFF()
relay2?.setOFF()
if (filename!=null){
try{
val player= AudioFilePlayer(audioID, filename)
player.Play { _ -> afp = null}
afp = player
afp= AudioFilePlayer(audioID, filename)
afp?.Play { _ ->
afp = null
relay1?.setOFF()
relay2?.setOFF()
}
relay1?.setON()
relay2?.setON()
WsReply(cmd.command,"success")
} catch (e: Exception){
afp?.Stop()
afp = null
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" ->{
commandLED?.Blink()
afp?.Stop()
afp = null
relay1?.setOFF()
relay2?.setOFF()
WsReply(cmd.command,"success")
}
else -> WsReply(cmd.command,"Invalid command: ${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 source: $source")
@@ -151,76 +491,11 @@ fun main() {
} , Pair("admin","admin1234"))
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
import audio.AudioUtility.Companion.bass
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import somecodes.Codes
import java.util.function.Consumer
import kotlin.io.path.absolutePathString
import kotlin.io.path.exists
/**
* Audio Player for playing audio files.
* Supported extensions : .wav, .mp3
*/
@Suppress("unused")
class AudioFilePlayer(deviceID: Int, val filename: String?, device_samplingrate: Int = 48000) {
val bass: Bass = Bass.Instance
var filehandle = 0
class AudioFilePlayer(deviceID: Int, val filename: String, device_samplingrate: Int = 48000) {
private var filehandle = 0
var isPlaying = false
val fileSize: Long
val duration: Double
var elapsed: Double = 0.0
init{
if (bass.BASS_SetDevice(deviceID)){
filehandle = bass.BASS_StreamCreateFile(false, filename, 0, 0, 0)
if (filehandle == 0) {
throw Exception("Failed to create stream for file $filename: ${bass.BASS_ErrorGetCode()}")
}
} else throw Exception("Failed to set device $deviceID")
val fullpath = Codes.audioFilePath.resolve(filename)
if (fullpath.exists()){
if (AudioUtility.InitDevice(deviceID, device_samplingrate)) {
AudioUtility.setVolumeOutput(deviceID,1.0f)
filehandle = bass.BASS_StreamCreateFile(false, fullpath.absolutePathString(), 0, 0, 0)
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(){
if (filehandle!=0){
bass.BASS_ChannelStop(filehandle)
bass.BASS_StreamFree(filehandle)
bass.BASS_ChannelFree(filehandle)
filehandle = 0
}
isPlaying = false
AudioUtility.Free()
}
fun Play(finished: Consumer<Any> ) : Boolean{
if (bass.BASS_ChannelPlay(filehandle, false)){
bass.BASS_ChannelSetAttribute(filehandle, Bass.BASS_ATTRIB_VOL, 1.0f)
elapsed = 0.0
CoroutineScope(Dispatchers.Default).launch {
isPlaying = true
while(true){
delay(1000)
delay(50)
if (bass.BASS_ChannelIsActive(filehandle)!= Bass.BASS_ACTIVE_PLAYING){
// finished playing
break
}
elapsed = bass.BASS_ChannelBytes2Seconds(filehandle, bass.BASS_ChannelGetPosition(filehandle, Bass.BASS_POS_BYTE))
}
isPlaying = false
bass.BASS_StreamFree(filehandle)
bass.BASS_ChannelFree(filehandle)
filehandle = 0
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 false

View File

@@ -1,13 +1,62 @@
package audio
import com.sun.jna.Platform
import org.slf4j.Logger
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 {
private val bass = Bass.Instance
companion object{
val bass : Bass = Bass.Instance
private val logger : Logger = LoggerFactory.getLogger(AudioUtility::class.java)
init{
logger.info("Bass Version = ${bass.BASS_GetVersion().toHexString()}")
fun ExtractLibraries(parent: String, filename: String, targetPath: Path) : Path?{
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>> {
@@ -23,6 +72,14 @@ class AudioUtility {
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 {
val dev = Bass.BASS_DEVICEINFO()
if (bass.BASS_GetDeviceInfo(deviceID, dev)) {
@@ -38,4 +95,23 @@ class AudioUtility {
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 {
Bass Instance = Native.load("bass", Bass.class);
int BASSVERSION = 0x204; // API version
String BASSVERSIONTEXT = "2.4";

View File

@@ -1,5 +1,6 @@
package audio
import audio.AudioUtility.Companion.bass
import com.sun.jna.Memory
import com.sun.jna.Pointer
import org.slf4j.Logger
@@ -12,37 +13,40 @@ import org.slf4j.LoggerFactory
* @throws Exception if the device cannot be set.
*/
@Suppress("unused")
class OpusStreamReceiver(deviceID: Int, val samplingrate: Int = 16000) {
private val bass = Bass.Instance
private val bassopus = BASSOPUS.Instance
class OpusStreamReceiver(val deviceID: Int, val samplingrate: Int = 16000) {
private var filehandle = 0
private val logger : Logger = LoggerFactory.getLogger(OpusStreamReceiver::class.java)
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.
* @return true if the stream started successfully, false otherwise.
*/
fun Start() : Boolean{
if (AudioUtility.InitDevice(deviceID, samplingrate)){
val opushead = BASSOPUS.BASS_OPUS_HEAD()
opushead.version = 1
opushead.channels = 1
opushead.inputrate = samplingrate
val procpush = Pointer(-1)
AudioUtility.setVolumeOutput(deviceID, 1.0f)
filehandle = bassopus.BASS_OPUS_StreamCreate(opushead,0, procpush, null)
if (filehandle != 0){
if (bass.BASS_ChannelPlay(filehandle,false)){
bass.BASS_ChannelSetAttribute(filehandle, Bass.BASS_ATTRIB_VOL, 1.0f)
isPlaying = true
return true
} 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("Error initializing device $deviceID with sampling rate $samplingrate, code ${bass.BASS_ErrorGetCode()}")
return false
}
@@ -51,11 +55,11 @@ class OpusStreamReceiver(deviceID: Int, val samplingrate: Int = 16000) {
*/
fun Stop(){
if (filehandle!=0){
bass.BASS_ChannelStop(filehandle)
bass.BASS_StreamFree(filehandle)
bass.BASS_ChannelFree(filehandle)
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
import com.fasterxml.jackson.databind.ObjectMapper
import com.sun.jna.Platform
import org.slf4j.LoggerFactory
import java.io.File
import java.nio.file.Path
import kotlin.io.path.Path
@Suppress("unused")
class Codes {
private val objectMapper = ObjectMapper()
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 {
if (s!=null){

View File

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

View File

@@ -2,10 +2,15 @@ package web;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.javalin.Javalin;
import io.javalin.http.HttpStatus;
import javafx.util.Pair;
import org.slf4j.Logger;
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.Map;
import java.util.function.BiFunction;
@@ -30,7 +35,7 @@ public class webApp {
}
app = Javalin.create(config -> {
config.useVirtualThreads = true; // Enable virtual threads for better performance
config.staticFiles.add("/");
config.staticFiles.add("/webpage");
config.router.apiBuilder(()->{
path("/", () -> get(ctx -> {
if (ctx.sessionAttribute("user") == null) {
@@ -66,7 +71,7 @@ public class webApp {
String message = wsMessageContext.message();
try{
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);
wsMessageContext.send(reply);
} catch (Exception e){
@@ -85,7 +90,7 @@ public class webApp {
String message = wsMessageContext.message();
try{
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);
wsMessageContext.send(reply);
} catch (Exception e){
@@ -99,14 +104,33 @@ public class webApp {
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 -> {
// Handle incoming WebSocket messages
String message = wsMessageContext.message();
//logger.info("Received message from setting.html/ws: {}", message);
try{
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);
logger.info("Replying to setting.html/ws : {}", reply);
//logger.info("Replying to setting.html/ws : {}", reply);
wsMessageContext.send(reply);
} catch (Exception e){
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.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.handshake.ServerHandshake
import org.slf4j.Logger
@@ -20,23 +16,27 @@ import java.util.function.BiConsumer
/**
* ZelloClient is a WebSocket client for connecting to Zello services.
* [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 imageJob = HashMap<Int, ZelloImageJob>()
private val commandJob = HashMap<Int, Any>()
companion object {
fun fromConsumerZello(username : String, password: String) : ZelloClient {
return ZelloClient(URI.create("wss://zello.io/ws"), username, password)
fun fromConsumerZello(username : String, password: String, channel: String) : ZelloClient {
return ZelloClient(URI.create("wss://zello.io/ws"), username, password, channel)
}
fun fromZelloWork(username: String, password: String, networkName : String) : ZelloClient{
return ZelloClient(URI.create("wss://zellowork.io/ws/$networkName"), username, password)
fun fromZelloWork(username: String, password: String, channel: String, networkName : String) : ZelloClient{
return ZelloClient(URI.create("wss://zellowork.io/ws/$networkName"), username, password, channel)
}
fun fromZelloEnterpriseServer(username: String, password: String, serverDomain: String) : ZelloClient{
return ZelloClient(URI.create("wss://$serverDomain/ws/mesh"), username, password)
fun fromZelloEnterpriseServer(username: String, password: String, channel: String, serverDomain: String) : ZelloClient{
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
// 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"
// default channel to join
private var channels = arrayOf("GtcDev2025")
// refresh token for the session
// this is set after the first LogonReply
private var refresh_token: String? = null
private val logger : Logger = LoggerFactory.getLogger(ZelloClient::class.java)
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){
client = object : WebSocketClient(address) {
override fun onOpen(handshakedata: ServerHandshake?) {
//logger.info("Connected to $address")
isOnline = false
currentChannel = null
isReceivingStreaming = false
seqID = 0
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)
send(value)
}
@@ -82,6 +90,7 @@ class ZelloClient(val address : URI, val username: String, val password: String)
job.pushAudioData(data)
event.onStreamingData(job.from, job.For?:"", job.channel, data)
}
bytesReceived += data.size
}
0x02.toByte() ->{
@@ -119,7 +128,7 @@ class ZelloClient(val address : URI, val username: String, val password: String)
@Suppress("UNCHECKED_CAST")
override fun onMessage(message: String?) {
logger.info("Message received: $message")
//logger.info("Message received: $message")
val jsnode = mapper.readTree(message)
if (jsnode["seq"] != null) {
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
event.onConnected()
} else {
logger.error("Failed to logon: ${lgreply.error ?: "Unknown error"}")
event.onError("Failed to logon: ${lgreply.error ?: "Unknown error"}")
}
}
else ->{
@@ -163,6 +172,8 @@ class ZelloClient(val address : URI, val username: String, val password: String)
"on_channel_status" -> {
val channelstatus = mapper.treeToValue(jsnode, Event_OnChannelStatus::class.java)
event.onChannelStatus(channelstatus.channel, channelstatus.status, channelstatus.users_online, channelstatus.error, channelstatus.error_type)
currentChannel = channelstatus.channel
isOnline = channelstatus.status== "online"
}
"on_error" -> {
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}")
streamJob[streamstart.stream_id] = ZelloAudioJob.fromEventOnStreamStart(streamstart)
event.onStartStreaming(streamstart.from, streamstart.For, streamstart.channel)
isReceivingStreaming = true
receivingFrom = streamstart.from
bytesReceived = 0 // reset bytes received
}
"on_stream_stop" -> {
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.onAudioData(job.streamID, job.from, job.For?:"", job.channel, job.getAudioData())
}
isReceivingStreaming = false
receivingFrom = null
}
"on_image" ->{
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) {
logger.info("Closed from ${if (remote) "Remote side" else "Local side"}, Code=$code, Reason=$reason")
event.onDisconnected()
// try reconnecting after 10 seconds
CoroutineScope(Dispatchers.Default).launch {
delay(10000)
connect()
}
event.onDisconnected(reason?: "Unknown reason")
isOnline = false
currentChannel = null
//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?) {

View File

@@ -9,7 +9,7 @@ interface ZelloEvent {
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 onConnected()
fun onDisconnected()
fun onDisconnected(reason: String)
fun onError(errorMessage: String)
fun onStartStreaming(from: String, For: String, channel: String)
fun onStopStreaming(from: String, For: String, channel: String)