Compare commits

...

4 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
42 changed files with 1382 additions and 51 deletions

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

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

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

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

View File

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

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

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

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

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

View File

@@ -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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,23 +12,124 @@ import zello.ZelloEvent
import javafx.util.Pair import javafx.util.Pair
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import sbc.DigitalOutput
import sbc.NanopiDuo2
import sbc.SbcInfo
import sbc.cpuinfo
import sbc.meminfo
import sbc.netdev
import somecodes.Codes import somecodes.Codes
import somecodes.Codes.Companion.isLinux
import web.WsCommand import web.WsCommand
import java.util.function.BiFunction import java.util.function.BiFunction
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or //TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter. // click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
fun main() { fun main() {
// change when updating the application
val appversion = "0.1.1"
val logger = LoggerFactory.getLogger("Main") val logger = LoggerFactory.getLogger("Main")
val objectMapper = jacksonObjectMapper() val objectMapper = jacksonObjectMapper()
logger.info("Application started, version $appversion")
val cfg = configFile() val cfg = configFile()
cfg.Load() cfg.Load()
/**
* Coroutine scope for the application
*/
val appscope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
/**
* various SBC related functions and information
*/
var sbc : SbcInfo?
/**
* cpu information, one time read
*/
var cpuinfo: cpuinfo?
/**
* ram information, will be updated periodically in appscope
*/
var raminfo: meminfo?
/**
* cpu statistics, will be updated periodically in appscope
* key is cpu name
* value is cpu utilization percentage
* e.g. cpustat["cpu0"] = 12.5
*/
val cpustat = HashMap<String, Double>()
/**
* network statistics, will be updated periodically in appscope
* key is network interface name
* value is a Pair of strings, first is RX transfer speed per second, second is TX transfer speed per second
*/
val networkstat = HashMap<String, Pair<String, String>>()
if (isLinux()){
sbc = SbcInfo()
cpuinfo = sbc.getCpuInfo()
//logger.info("CPU Info: ${cpuinfo?.hardware?:"N/A"}, Cores: ${cpuinfo?.core?:"N/A"}, SerialNumber: ${cpuinfo?.serialNumber?:"N/A"}")
appscope.launch {
// run while the application is active
while(isActive){
raminfo = sbc.getMemInfo()
//logger.info(raminfo?.toString() ?: "Failed to get RAM info")
sbc.getCpuUtilization { it ->
// result will come 1 second later, as array of pairs,
// with first being cpu name and second being utilization percentage
it.forEach { cpuinfo ->
cpustat[cpuinfo.first] = cpuinfo.second
}
//logger.info("CPU Utilization: ${cpustat.map { "${it.key.ifBlank { "avg" }}: ${ it.value}%" }}")
}
sbc.getNetworkTransferSpeed { it ->
// result will come 1 second later, as map of network interface name to Pair of RX and TX speeds
it.forEach { xx ->
networkstat[xx.key] = Pair(
netdev.toText(xx.value.first)+ "/s",
netdev.toText(xx.value.second) + "/s"
)
}
//logger.info("Network Transfer Speed: ${networkstat.map { "${it.key}: RX=${it.value.key}, TX=${it.value.value}" }}")
}
delay(5000) // Update every 5 seconds
}
}
}
var relay1 : DigitalOutput? = null
var relay2 : DigitalOutput? = null
var commandLED : DigitalOutput? = null
var streamingLED : DigitalOutput? = null
appscope.launch {
relay1 = DigitalOutput(NanopiDuo2.CLK.linuxGpio, "Relay1", true)
relay2 = DigitalOutput(NanopiDuo2.MISO.linuxGpio, "Relay2", true)
commandLED = DigitalOutput(NanopiDuo2.TX1.linuxGpio, "CommandLED", true)
streamingLED = DigitalOutput(NanopiDuo2.RX1.linuxGpio, "StreamingLED", true)
relay1.setOFF()
relay2.setOFF()
commandLED.setOFF()
streamingLED.setOFF()
}
var audioID = 0 var audioID = 0
val preferedAudioDevice = "Speakers" val preferedAudioDevice = "USB Audio"
AudioUtility.LoadLibraries()
AudioUtility.PrintVersion()
AudioUtility.DetectPlaybackDevices().forEach { pair -> AudioUtility.DetectPlaybackDevices().forEach { pair ->
logger.info("Device ID: ${pair.first}, Name: ${pair.second}") logger.info("Device ID: ${pair.first}, Name: ${pair.second}")
@@ -36,10 +137,9 @@ fun main() {
audioID = pair.first audioID = pair.first
} }
} }
if (audioID!=0){ if (audioID==0) audioID = 1 // fallback to first device if preferred not found
val initsuccess = AudioUtility.InitDevice(audioID,44100) val initsuccess = AudioUtility.InitDevice(audioID,44100)
logger.info("Audio Device $audioID initialized: $initsuccess") logger.info("Audio Device $audioID initialized: $initsuccess")
}
// for Zello Client // for Zello Client
val o = OpusStreamReceiver(audioID) val o = OpusStreamReceiver(audioID)
@@ -113,19 +213,24 @@ fun main() {
override fun onConnected() { override fun onConnected() {
logger.info("Connected to Zello server.") logger.info("Connected to Zello server.")
relay1?.setOFF()
relay2?.setOFF()
} }
override fun onDisconnected(reason: String) { override fun onDisconnected(reason: String) {
logger.info("Disconnected from Zello Server, reason: $reason") logger.info("Disconnected from Zello Server, reason: $reason")
logger.info("Reconnecting after 10 seconds...") logger.info("Reconnecting after 10 seconds...")
z.Stop() z.Stop()
relay1?.setOFF()
relay2?.setOFF()
val e = this val e = this
CoroutineScope(Dispatchers.Default).launch {
appscope.launch {
delay(10000) // Wait for 10 seconds before trying to reconnect delay(10000) // Wait for 10 seconds before trying to reconnect
z = CreateZelloFromConfig() z = CreateZelloFromConfig()
z.Start(e) z.Start(e)
} }
} }
override fun onError(errorMessage: String) { override fun onError(errorMessage: String) {
@@ -136,8 +241,10 @@ fun main() {
// stop any previous playback // stop any previous playback
afp?.Stop() afp?.Stop()
afp = null afp = null
commandLED?.Blink()
if (o.Start()){ if (o.Start()){
relay1?.setON()
relay2?.setON()
logger.info("Opus Receiver ready for streaming from $from for $For on channel $channel") logger.info("Opus Receiver ready for streaming from $from for $For on channel $channel")
} else { } else {
logger.info("Failed to start Opus Receiver for streaming from $from for $For on channel $channel") logger.info("Failed to start Opus Receiver for streaming from $from for $For on channel $channel")
@@ -146,6 +253,9 @@ fun main() {
override fun onStopStreaming(from: String, For: String, channel: String) { override fun onStopStreaming(from: String, For: String, channel: String) {
o.Stop() o.Stop()
relay1?.setON()
relay2?.setOFF()
commandLED?.Blink()
logger.info("Opus Receiver stopped streaming from $from for $For on channel $channel") logger.info("Opus Receiver stopped streaming from $from for $For on channel $channel")
} }
@@ -156,6 +266,7 @@ fun main() {
data: ByteArray data: ByteArray
) { ) {
if (o.isPlaying) o.PushData(data) if (o.isPlaying) o.PushData(data)
streamingLED?.Blink()
} }
} }
z.Start(z_event) z.Start(z_event)
@@ -166,6 +277,7 @@ fun main() {
when (source) { when (source) {
"setting" -> when(cmd.command){ "setting" -> when(cmd.command){
"getConfig" ->{ "getConfig" ->{
commandLED?.Blink()
val data = mapOf( val data = mapOf(
"zelloUsername" to cfg.ZelloUsername, "zelloUsername" to cfg.ZelloUsername,
"zelloPassword" to cfg.ZelloPassword, "zelloPassword" to cfg.ZelloPassword,
@@ -187,6 +299,7 @@ fun main() {
WsReply(cmd.command, objectMapper.writeValueAsString(data).trim()) WsReply(cmd.command, objectMapper.writeValueAsString(data).trim())
} }
"setZelloConfig" -> { "setZelloConfig" -> {
commandLED?.Blink()
try{ try{
val xx = objectMapper.readValue(cmd.data, object: TypeReference<Map<String, String>>() {}) val xx = objectMapper.readValue(cmd.data, object: TypeReference<Map<String, String>>() {})
var changed = false var changed = false
@@ -220,6 +333,7 @@ fun main() {
z = CreateZelloFromConfig() z = CreateZelloFromConfig()
z.Start(z_event) z.Start(z_event)
WsReply(cmd.command,"success") WsReply(cmd.command,"success")
} else WsReply(cmd.command,"No changes made") } else WsReply(cmd.command,"No changes made")
} catch (e: Exception){ } catch (e: Exception){
@@ -228,6 +342,7 @@ fun main() {
} }
"setMessageConfig"-> { "setMessageConfig"-> {
commandLED?.Blink()
try{ try{
val xx = objectMapper.readValue(cmd.data, object : TypeReference<Map<String, String>>() {}) val xx = objectMapper.readValue(cmd.data, object : TypeReference<Map<String, String>>() {})
@@ -278,6 +393,7 @@ fun main() {
"prerecordedbroadcast" -> when(cmd.command){ "prerecordedbroadcast" -> when(cmd.command){
"getMessageConfig" ->{ "getMessageConfig" ->{
commandLED?.Blink()
val data = mapOf( val data = mapOf(
"M1" to cfg.M1, "M1" to cfg.M1,
"M2" to cfg.M2, "M2" to cfg.M2,
@@ -291,6 +407,7 @@ fun main() {
WsReply(cmd.command, objectMapper.writeValueAsString(data)) WsReply(cmd.command, objectMapper.writeValueAsString(data))
} }
"getPlaybackStatus" ->{ "getPlaybackStatus" ->{
commandLED?.Blink()
if (afp!=null && true==afp?.isPlaying){ if (afp!=null && true==afp?.isPlaying){
WsReply(cmd.command, "Playing: ${afp?.filename}, Duration: ${afp?.duration?.toInt()}, Elapsed: ${afp?.elapsed?.toInt()} seconds") WsReply(cmd.command, "Playing: ${afp?.filename}, Duration: ${afp?.duration?.toInt()}, Elapsed: ${afp?.elapsed?.toInt()} seconds")
} else { } else {
@@ -298,7 +415,7 @@ fun main() {
} }
} }
"playMessage" ->{ "playMessage" ->{
commandLED?.Blink()
afp?.Stop() afp?.Stop()
afp = null afp = null
// stop Opus Receiver if it is running // stop Opus Receiver if it is running
@@ -317,14 +434,23 @@ fun main() {
null null
} }
} }
relay1?.setOFF()
relay2?.setOFF()
if (filename!=null){ if (filename!=null){
try{ try{
afp= AudioFilePlayer(audioID, filename) afp= AudioFilePlayer(audioID, filename)
afp?.Play { _ -> afp = null} afp?.Play { _ ->
afp = null
relay1?.setOFF()
relay2?.setOFF()
}
relay1?.setON()
relay2?.setON()
WsReply(cmd.command,"success") WsReply(cmd.command,"success")
} catch (e: Exception){ } catch (e: Exception){
afp?.Stop() afp?.Stop()
afp = null afp = null
WsReply(cmd.command, "failed: ${e.message}") WsReply(cmd.command, "failed: ${e.message}")
} }
@@ -336,14 +462,18 @@ fun main() {
} }
"stopMessage" ->{ "stopMessage" ->{
commandLED?.Blink()
afp?.Stop() afp?.Stop()
afp = null afp = null
relay1?.setOFF()
relay2?.setOFF()
WsReply(cmd.command,"success") WsReply(cmd.command,"success")
} }
else -> WsReply(cmd.command,"Invalid command: ${cmd.command}") else -> WsReply(cmd.command,"Invalid command: ${cmd.command}")
} }
"pocreceiver" -> when(cmd.command){ "pocreceiver" -> when(cmd.command){
"getZelloStatus" -> { "getZelloStatus" -> {
commandLED?.Blink()
var status = "Disconnected" var status = "Disconnected"
if (z.currentChannel?.isNotBlank() == true){ if (z.currentChannel?.isNotBlank() == true){
status = "Channel: ${z.currentChannel}, Online: ${z.isOnline}, Username: ${z.username}" status = "Channel: ${z.currentChannel}, Online: ${z.isOnline}, Username: ${z.username}"

View File

@@ -1,5 +1,6 @@
package audio package audio
import audio.AudioUtility.Companion.bass
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -15,7 +16,7 @@ import kotlin.io.path.exists
*/ */
@Suppress("unused") @Suppress("unused")
class AudioFilePlayer(deviceID: Int, val filename: String, device_samplingrate: Int = 48000) { class AudioFilePlayer(deviceID: Int, val filename: String, device_samplingrate: Int = 48000) {
private val bass: Bass = Bass.Instance
private var filehandle = 0 private var filehandle = 0
var isPlaying = false var isPlaying = false
val fileSize: Long val fileSize: Long
@@ -25,6 +26,7 @@ class AudioFilePlayer(deviceID: Int, val filename: String, device_samplingrate:
val fullpath = Codes.audioFilePath.resolve(filename) val fullpath = Codes.audioFilePath.resolve(filename)
if (fullpath.exists()){ if (fullpath.exists()){
if (AudioUtility.InitDevice(deviceID, device_samplingrate)) { if (AudioUtility.InitDevice(deviceID, device_samplingrate)) {
AudioUtility.setVolumeOutput(deviceID,1.0f)
filehandle = bass.BASS_StreamCreateFile(false, fullpath.absolutePathString(), 0, 0, 0) filehandle = bass.BASS_StreamCreateFile(false, fullpath.absolutePathString(), 0, 0, 0)
if (filehandle!=0){ if (filehandle!=0){
fileSize = bass.BASS_ChannelGetLength(filehandle, Bass.BASS_POS_BYTE) fileSize = bass.BASS_ChannelGetLength(filehandle, Bass.BASS_POS_BYTE)
@@ -47,6 +49,7 @@ class AudioFilePlayer(deviceID: Int, val filename: String, device_samplingrate:
fun Play(finished: Consumer<Any> ) : Boolean{ fun Play(finished: Consumer<Any> ) : Boolean{
if (bass.BASS_ChannelPlay(filehandle, false)){ if (bass.BASS_ChannelPlay(filehandle, false)){
bass.BASS_ChannelSetAttribute(filehandle, Bass.BASS_ATTRIB_VOL, 1.0f)
elapsed = 0.0 elapsed = 0.0
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
isPlaying = true isPlaying = true

View File

@@ -1,15 +1,63 @@
package audio package audio
import com.sun.jna.Platform
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.isRegularFile
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
@Suppress("unused")
class AudioUtility { class AudioUtility {
private val logger : Logger = LoggerFactory.getLogger(AudioUtility::class.java)
companion object{ companion object{
private val bass = Bass.Instance val bass : Bass = Bass.Instance
private val logger : Logger = LoggerFactory.getLogger(AudioUtility::class.java)
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>> { fun DetectPlaybackDevices() : List<Pair<Int, String>> {
val result = ArrayList<Pair<Int, String>>() val result = ArrayList<Pair<Int, String>>()
@@ -24,6 +72,14 @@ class AudioUtility {
return result return result
} }
fun setVolumeOutput(deviceID: Int, value: Float){
var vol = value
if (vol < 0) vol = 0f
if (vol > 1) vol = 1f
bass.BASS_SetDevice(deviceID)
bass.BASS_SetVolume(vol)
}
fun InitDevice(deviceID: Int, device_samplingrate: Int = 48000) : Boolean { fun InitDevice(deviceID: Int, device_samplingrate: Int = 48000) : Boolean {
val dev = Bass.BASS_DEVICEINFO() val dev = Bass.BASS_DEVICEINFO()
if (bass.BASS_GetDeviceInfo(deviceID, dev)) { if (bass.BASS_GetDeviceInfo(deviceID, dev)) {
@@ -54,9 +110,7 @@ class AudioUtility {
} }
} }
init{
logger.info("Bass Version = ${bass.BASS_GetVersion().toHexString()}")
}

View File

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

View File

@@ -1,5 +1,6 @@
package audio package audio
import audio.AudioUtility.Companion.bass
import com.sun.jna.Memory import com.sun.jna.Memory
import com.sun.jna.Pointer import com.sun.jna.Pointer
import org.slf4j.Logger import org.slf4j.Logger
@@ -13,11 +14,11 @@ import org.slf4j.LoggerFactory
*/ */
@Suppress("unused") @Suppress("unused")
class OpusStreamReceiver(val deviceID: Int, val samplingrate: Int = 16000) { class OpusStreamReceiver(val deviceID: Int, val samplingrate: Int = 16000) {
private val bass = Bass.Instance
private val bassopus = BASSOPUS.Instance
private var filehandle = 0 private var filehandle = 0
private val logger : Logger = LoggerFactory.getLogger(OpusStreamReceiver::class.java) private val logger : Logger = LoggerFactory.getLogger(OpusStreamReceiver::class.java)
var isPlaying = false var isPlaying = false
val bassopus : BASSOPUS = BASSOPUS.Instance
@@ -34,9 +35,12 @@ class OpusStreamReceiver(val deviceID: Int, val samplingrate: Int = 16000) {
opushead.channels = 1 opushead.channels = 1
opushead.inputrate = samplingrate opushead.inputrate = samplingrate
val procpush = Pointer(-1) val procpush = Pointer(-1)
AudioUtility.setVolumeOutput(deviceID, 1.0f)
filehandle = bassopus.BASS_OPUS_StreamCreate(opushead,0, procpush, null) filehandle = bassopus.BASS_OPUS_StreamCreate(opushead,0, procpush, null)
if (filehandle != 0){ if (filehandle != 0){
if (bass.BASS_ChannelPlay(filehandle,false)){ if (bass.BASS_ChannelPlay(filehandle,false)){
bass.BASS_ChannelSetAttribute(filehandle, Bass.BASS_ATTRIB_VOL, 1.0f)
isPlaying = true isPlaying = true
return true return true
} else logger.error("BASS_ChannelPlay failed for filehandle $filehandle, code ${bass.BASS_ErrorGetCode()}") } else logger.error("BASS_ChannelPlay failed for filehandle $filehandle, code ${bass.BASS_ErrorGetCode()}")

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,6 +1,9 @@
package somecodes package somecodes
import com.sun.jna.Platform
import org.slf4j.LoggerFactory
import java.io.File import java.io.File
import java.nio.file.Path
import kotlin.io.path.Path import kotlin.io.path.Path
@Suppress("unused") @Suppress("unused")
@@ -8,16 +11,35 @@ class Codes {
companion object{ companion object{
val audioFilePath = Path(System.getProperty("user.dir"), "audiofile") 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 val validAudioExtensions = setOf("wav", "mp3")
private val KB_size = 1024 // 1 KB = 1024 bytes private const val KB_size = 1024.0 // 1 KB = 1024 bytes
private val MB_size = 1024 * KB_size // 1 MB = 1024 KB private const val MB_size = 1024.0 * KB_size // 1 MB = 1024 KB
private val GB_size = 1024 * MB_size // 1 GB = 1024 MB 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 { fun SizeToString(size: Long) : String {
return when { return when {
size >= GB_size -> String.format("%.2f GB", size.toDouble() / GB_size) size >= GB_size -> String.format("%.2f GB", size / GB_size)
size >= MB_size -> String.format("%.2f MB", size.toDouble() / MB_size) size >= MB_size -> String.format("%.2f MB", size / MB_size)
size >= KB_size -> String.format("%.2f KB", size.toDouble() / KB_size) size >= KB_size -> String.format("%.2f KB", size / KB_size)
else -> "$size bytes" else -> "$size bytes"
} }
} }

View File

@@ -35,7 +35,7 @@ public class webApp {
} }
app = Javalin.create(config -> { app = Javalin.create(config -> {
config.useVirtualThreads = true; // Enable virtual threads for better performance config.useVirtualThreads = true; // Enable virtual threads for better performance
config.staticFiles.add("/"); config.staticFiles.add("/webpage");
config.router.apiBuilder(()->{ config.router.apiBuilder(()->{
path("/", () -> get(ctx -> { path("/", () -> get(ctx -> {
if (ctx.sessionAttribute("user") == null) { if (ctx.sessionAttribute("user") == null) {

View File

@@ -128,7 +128,7 @@ class ZelloClient(val address : URI, val username: String, val password: String,
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun onMessage(message: String?) { override fun onMessage(message: String?) {
logger.info("Message received: $message") //logger.info("Message received: $message")
val jsnode = mapper.readTree(message) val jsnode = mapper.readTree(message)
if (jsnode["seq"] != null) { if (jsnode["seq"] != null) {
when(val seq = jsnode.get("seq").asInt()){ when(val seq = jsnode.get("seq").asInt()){
@@ -229,19 +229,6 @@ class ZelloClient(val address : URI, val username: String, val password: String,
isOnline = false isOnline = false
currentChannel = null 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?) { override fun onError(ex: Exception?) {