Compare commits
6 Commits
662608550b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c32e48e04 | |||
| 60e8524c8f | |||
| f4c9fa8730 | |||
| 3b31044610 | |||
| eba4f7852e | |||
| 446f031535 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
### IntelliJ IDEA ###
|
### IntelliJ IDEA ###
|
||||||
out/
|
out/
|
||||||
|
audiofile/
|
||||||
!**/src/main/**/out/
|
!**/src/main/**/out/
|
||||||
!**/src/test/**/out/
|
!**/src/test/**/out/
|
||||||
|
|
||||||
|
|||||||
20
.idea/artifacts/EWS_Nanopi_Duo2.xml
generated
Normal file
20
.idea/artifacts/EWS_Nanopi_Duo2.xml
generated
Normal 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
15
.idea/deployment.xml
generated
Normal 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>
|
||||||
1
.idea/inspectionProfiles/Project_Default.xml
generated
1
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -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
8
.idea/sshConfigs.xml
generated
Normal 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
14
.idea/webServers.xml
generated
Normal 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>
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
|
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/testResources" type="java-test-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/testResources" type="java-test-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/html" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/html" isTestSource="false" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/audiofile" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
|||||||
16
config.json
16
config.json
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"ZelloUsername": "gtcdevice01",
|
|
||||||
"ZelloPassword": "GtcDev2025",
|
|
||||||
"ZelloChannel": "GtcDev2025",
|
|
||||||
"ZelloServer": "community",
|
|
||||||
"ZelloWorkNetworkName": "",
|
|
||||||
"ZelloEnterpriseServerDomain": "",
|
|
||||||
"M1": "",
|
|
||||||
"M2": "",
|
|
||||||
"M3": "",
|
|
||||||
"M4": "",
|
|
||||||
"M5": "",
|
|
||||||
"M6": "",
|
|
||||||
"M7": "",
|
|
||||||
"M8": ""
|
|
||||||
}
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
109
html/webpage/assets/js/hardwarestatus.js
Normal file
109
html/webpage/assets/js/hardwarestatus.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
// Your code here
|
// Your code here
|
||||||
console.log('pocreceiver.js is ready!');
|
//console.log('pocreceiver.js is ready!');
|
||||||
const path = window.location.pathname;
|
const path = window.location.pathname;
|
||||||
const ws = new WebSocket('ws://' + window.location.host + path + '/ws');
|
const ws = new WebSocket('ws://' + window.location.host + path + '/ws');
|
||||||
|
|
||||||
ws.onopen = function() {
|
ws.onopen = function() {
|
||||||
console.log('WebSocket connection opened');
|
//console.log('WebSocket connection opened');
|
||||||
$('#indicatorDisconnected').addClass('visually-hidden');
|
$('#indicatorDisconnected').addClass('visually-hidden');
|
||||||
$('#indicatorConnected').removeClass('visually-hidden');
|
$('#indicatorConnected').removeClass('visually-hidden');
|
||||||
setInterval(function() {
|
setInterval(function() {
|
||||||
sendCommand({ command: "getZelloStatus" });
|
sendCommand({ command: "getZelloStatus" });
|
||||||
}, 5000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = function(event) {
|
ws.onmessage = function(event) {
|
||||||
console.log('WebSocket message received:', event.data);
|
//console.log('WebSocket message received:', event.data);
|
||||||
let msg = {};
|
let msg = {};
|
||||||
try {
|
try {
|
||||||
msg = JSON.parse(event.data);
|
msg = JSON.parse(event.data);
|
||||||
@@ -25,13 +25,13 @@ $(document).ready(function() {
|
|||||||
}
|
}
|
||||||
if (msg.reply === "getZelloStatus" && msg.data !== undefined && msg.data.length > 0) {
|
if (msg.reply === "getZelloStatus" && msg.data !== undefined && msg.data.length > 0) {
|
||||||
const zelloData = msg.data;
|
const zelloData = msg.data;
|
||||||
console.log('Zello Status Data:', zelloData);
|
//console.log('Zello Status Data:', zelloData);
|
||||||
$('#zelloStatus').text(zelloData.status);
|
$('#zelloStatus').text(zelloData);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = function() {
|
ws.onclose = function() {
|
||||||
console.log('WebSocket connection closed');
|
//console.log('WebSocket connection closed');
|
||||||
$('#indicatorDisconnected').removeClass('visually-hidden');
|
$('#indicatorDisconnected').removeClass('visually-hidden');
|
||||||
$('#indicatorConnected').addClass('visually-hidden');
|
$('#indicatorConnected').addClass('visually-hidden');
|
||||||
};
|
};
|
||||||
114
html/webpage/assets/js/prerecordedbroadcast.js
Normal file
114
html/webpage/assets/js/prerecordedbroadcast.js
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
212
html/webpage/assets/js/setting.js
Normal file
212
html/webpage/assets/js/setting.js
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
260
html/webpage/hardwarestatus.html
Normal file
260
html/webpage/hardwarestatus.html
Normal 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>
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
<li class="nav-item"><a class="nav-link active" href="#">POC Receiver</a></li>
|
<li class="nav-item"><a class="nav-link active" href="#">POC Receiver</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="prerecordedbroadcast.html">Pre-Recorded Broadcast</a></li>
|
<li class="nav-item"><a class="nav-link" href="prerecordedbroadcast.html">Pre-Recorded Broadcast</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="setting.html">Setting and Content</a></li>
|
<li class="nav-item"><a class="nav-link" href="setting.html">Setting and Content</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="hardwarestatus.html">Hardware Status</a></li>
|
||||||
</ul><a class="btn btn-primary" role="button" href="index.html">Logout<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor" class="visually-hidden" id="indicatorDisconnected" style="color: var(--bs-danger);width: 26px;height: 26px;font-weight: bold;">
|
</ul><a class="btn btn-primary" role="button" href="index.html">Logout<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor" class="visually-hidden" id="indicatorDisconnected" style="color: var(--bs-danger);width: 26px;height: 26px;font-weight: bold;">
|
||||||
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
|
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
|
||||||
<path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c-9.4 9.4-9.4 24.6 0 33.9l47 47-47 47c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l47-47 47 47c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-47-47 47-47c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-47 47-47-47c-9.4-9.4-24.6-9.4-33.9 0z"></path>
|
<path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c-9.4 9.4-9.4 24.6 0 33.9l47 47-47 47c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l47-47 47 47c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-47-47 47-47c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-47 47-47-47c-9.4-9.4-24.6-9.4-33.9 0z"></path>
|
||||||
@@ -35,7 +36,7 @@
|
|||||||
<h1 class="text-center">Zello Status</h1>
|
<h1 class="text-center">Zello Status</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p id="zelloStatus">Paragraph</p>
|
<p class="d-flex justify-content-center" id="zelloStatus">No Status</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
|
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
<li class="nav-item"><a class="nav-link" href="pocreceiver.html">POC Receiver</a></li>
|
<li class="nav-item"><a class="nav-link" href="pocreceiver.html">POC Receiver</a></li>
|
||||||
<li class="nav-item"><a class="nav-link active" href="#">Pre-Recorded Broadcast</a></li>
|
<li class="nav-item"><a class="nav-link active" href="#">Pre-Recorded Broadcast</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="setting.html">Setting and Content</a></li>
|
<li class="nav-item"><a class="nav-link" href="setting.html">Setting and Content</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="hardwarestatus.html">Hardware Status</a></li>
|
||||||
</ul><a class="btn btn-primary" role="button" href="index.html">Logout<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor" class="visually-hidden" id="indicatorDisconnected" style="color: var(--bs-danger);width: 26px;height: 26px;font-weight: bold;">
|
</ul><a class="btn btn-primary" role="button" href="index.html">Logout<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor" class="visually-hidden" id="indicatorDisconnected" style="color: var(--bs-danger);width: 26px;height: 26px;font-weight: bold;">
|
||||||
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
|
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. -->
|
||||||
<path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c-9.4 9.4-9.4 24.6 0 33.9l47 47-47 47c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l47-47 47 47c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-47-47 47-47c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-47 47-47-47c-9.4-9.4-24.6-9.4-33.9 0z"></path>
|
<path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c-9.4 9.4-9.4 24.6 0 33.9l47 47-47 47c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l47-47 47 47c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-47-47 47-47c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-47 47-47-47c-9.4-9.4-24.6-9.4-33.9 0z"></path>
|
||||||
@@ -43,7 +44,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body" id="cardM1">
|
<div class="card-body" id="cardM1">
|
||||||
<h4 class="d-flex justify-content-center card-title">Message1</h4>
|
<h4 class="d-flex justify-content-center card-title">Message1</h4>
|
||||||
<p class="text-start card-text" id="fileM1">File 01</p>
|
<p class="d-flex justify-content-center card-text" id="fileM1">File 01</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM1" type="button">Play</button></div>
|
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM1" type="button">Play</button></div>
|
||||||
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM1" type="button">Stop</button></div>
|
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM1" type="button">Stop</button></div>
|
||||||
@@ -55,7 +56,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body" id="cardM2">
|
<div class="card-body" id="cardM2">
|
||||||
<h4 class="d-flex justify-content-center card-title">Message2</h4>
|
<h4 class="d-flex justify-content-center card-title">Message2</h4>
|
||||||
<p class="text-start card-text" id="fileM2">File 02</p>
|
<p class="d-flex justify-content-center card-text" id="fileM2">File 02</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM2" type="button">Play</button></div>
|
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM2" type="button">Play</button></div>
|
||||||
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM2" type="button">Stop</button></div>
|
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM2" type="button">Stop</button></div>
|
||||||
@@ -69,7 +70,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body" id="cardM3">
|
<div class="card-body" id="cardM3">
|
||||||
<h4 class="d-flex justify-content-center card-title">Message3</h4>
|
<h4 class="d-flex justify-content-center card-title">Message3</h4>
|
||||||
<p class="text-start card-text" id="fileM3">File 03</p>
|
<p class="d-flex justify-content-center card-text" id="fileM3">File 03</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM3" type="button">Play</button></div>
|
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM3" type="button">Play</button></div>
|
||||||
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM3" type="button">Stop</button></div>
|
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM3" type="button">Stop</button></div>
|
||||||
@@ -81,7 +82,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body" id="cardM4">
|
<div class="card-body" id="cardM4">
|
||||||
<h4 class="d-flex justify-content-center card-title">Message4</h4>
|
<h4 class="d-flex justify-content-center card-title">Message4</h4>
|
||||||
<p class="text-start card-text" id="fileM4">File 04</p>
|
<p class="d-flex justify-content-center card-text" id="fileM4">File 04</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM4" type="button">Play</button></div>
|
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM4" type="button">Play</button></div>
|
||||||
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM4" type="button">Stop</button></div>
|
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM4" type="button">Stop</button></div>
|
||||||
@@ -95,7 +96,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body" id="cardM5">
|
<div class="card-body" id="cardM5">
|
||||||
<h4 class="d-flex justify-content-center card-title">Message5</h4>
|
<h4 class="d-flex justify-content-center card-title">Message5</h4>
|
||||||
<p class="text-start card-text" id="fileM5">File 05</p>
|
<p class="d-flex justify-content-center card-text" id="fileM5">File 05</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM5" type="button">Play</button></div>
|
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM5" type="button">Play</button></div>
|
||||||
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM5" type="button">Stop</button></div>
|
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM5" type="button">Stop</button></div>
|
||||||
@@ -107,7 +108,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body" id="cardM6">
|
<div class="card-body" id="cardM6">
|
||||||
<h4 class="d-flex justify-content-center card-title">Message6</h4>
|
<h4 class="d-flex justify-content-center card-title">Message6</h4>
|
||||||
<p class="text-start card-text" id="fileM6">File 06</p>
|
<p class="d-flex justify-content-center card-text" id="fileM6">File 06</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM6" type="button">Play</button></div>
|
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM6" type="button">Play</button></div>
|
||||||
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM6" type="button">Stop</button></div>
|
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM6" type="button">Stop</button></div>
|
||||||
@@ -121,7 +122,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body" id="cardM7">
|
<div class="card-body" id="cardM7">
|
||||||
<h4 class="d-flex justify-content-center card-title">Message7</h4>
|
<h4 class="d-flex justify-content-center card-title">Message7</h4>
|
||||||
<p class="text-start card-text" id="fileM7">File 07</p>
|
<p class="d-flex justify-content-center card-text" id="fileM7">File 07</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM7" type="button">Play</button></div>
|
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM7" type="button">Play</button></div>
|
||||||
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM7" type="button">Stop</button></div>
|
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM7" type="button">Stop</button></div>
|
||||||
@@ -133,7 +134,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body" id="cardM8">
|
<div class="card-body" id="cardM8">
|
||||||
<h4 class="d-flex justify-content-center card-title">Message8</h4>
|
<h4 class="d-flex justify-content-center card-title">Message8</h4>
|
||||||
<p class="text-start card-text" id="fileM8">File 08</p>
|
<p class="d-flex justify-content-center card-text" id="fileM8">File 08</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM8" type="button">Play</button></div>
|
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="playM8" type="button">Play</button></div>
|
||||||
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM8" type="button">Stop</button></div>
|
<div class="col d-flex justify-content-center"><button class="btn btn-primary w-75" id="stopM8" type="button">Stop</button></div>
|
||||||
@@ -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 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>
|
||||||
27
meta/nanopi duo2/META-INF/MANIFEST.MF
Normal file
27
meta/nanopi duo2/META-INF/MANIFEST.MF
Normal 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
|
||||||
|
|
||||||
479
src/Main.kt
479
src/Main.kt
@@ -2,7 +2,7 @@ import audio.AudioFilePlayer
|
|||||||
import audio.AudioUtility
|
import audio.AudioUtility
|
||||||
import audio.OpusStreamReceiver
|
import audio.OpusStreamReceiver
|
||||||
import com.fasterxml.jackson.core.type.TypeReference
|
import com.fasterxml.jackson.core.type.TypeReference
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import somecodes.Codes.Companion.ValidString
|
import somecodes.Codes.Companion.ValidString
|
||||||
import somecodes.configFile
|
import somecodes.configFile
|
||||||
import web.WsReply
|
import web.WsReply
|
||||||
@@ -10,73 +10,379 @@ import web.webApp
|
|||||||
import zello.ZelloClient
|
import zello.ZelloClient
|
||||||
import zello.ZelloEvent
|
import zello.ZelloEvent
|
||||||
import javafx.util.Pair
|
import javafx.util.Pair
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import somecodes.Codes.Companion.ValidFile
|
import sbc.DigitalOutput
|
||||||
|
import sbc.NanopiDuo2
|
||||||
|
import sbc.SbcInfo
|
||||||
|
import sbc.cpuinfo
|
||||||
|
import sbc.meminfo
|
||||||
|
import sbc.netdev
|
||||||
|
import somecodes.Codes
|
||||||
|
import somecodes.Codes.Companion.isLinux
|
||||||
import web.WsCommand
|
import web.WsCommand
|
||||||
import java.util.function.BiFunction
|
import java.util.function.BiFunction
|
||||||
|
|
||||||
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
|
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
|
||||||
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
|
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
|
||||||
fun main() {
|
fun main() {
|
||||||
|
// change when updating the application
|
||||||
|
val appversion = "0.1.1"
|
||||||
val logger = LoggerFactory.getLogger("Main")
|
val logger = LoggerFactory.getLogger("Main")
|
||||||
val objectMapper = ObjectMapper()
|
val objectMapper = jacksonObjectMapper()
|
||||||
|
logger.info("Application started, version $appversion")
|
||||||
|
|
||||||
val cfg = configFile()
|
val cfg = configFile()
|
||||||
cfg.Load()
|
cfg.Load()
|
||||||
val au = AudioUtility()
|
|
||||||
|
/**
|
||||||
|
* Coroutine scope for the application
|
||||||
|
*/
|
||||||
|
val appscope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* various SBC related functions and information
|
||||||
|
*/
|
||||||
|
var sbc : SbcInfo?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cpu information, one time read
|
||||||
|
*/
|
||||||
|
var cpuinfo: cpuinfo?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ram information, will be updated periodically in appscope
|
||||||
|
*/
|
||||||
|
var raminfo: meminfo?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cpu statistics, will be updated periodically in appscope
|
||||||
|
* key is cpu name
|
||||||
|
* value is cpu utilization percentage
|
||||||
|
* e.g. cpustat["cpu0"] = 12.5
|
||||||
|
*/
|
||||||
|
val cpustat = HashMap<String, Double>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* network statistics, will be updated periodically in appscope
|
||||||
|
* key is network interface name
|
||||||
|
* value is a Pair of strings, first is RX transfer speed per second, second is TX transfer speed per second
|
||||||
|
*/
|
||||||
|
val networkstat = HashMap<String, Pair<String, String>>()
|
||||||
|
|
||||||
|
if (isLinux()){
|
||||||
|
sbc = SbcInfo()
|
||||||
|
cpuinfo = sbc.getCpuInfo()
|
||||||
|
//logger.info("CPU Info: ${cpuinfo?.hardware?:"N/A"}, Cores: ${cpuinfo?.core?:"N/A"}, SerialNumber: ${cpuinfo?.serialNumber?:"N/A"}")
|
||||||
|
|
||||||
|
appscope.launch {
|
||||||
|
// run while the application is active
|
||||||
|
while(isActive){
|
||||||
|
raminfo = sbc.getMemInfo()
|
||||||
|
//logger.info(raminfo?.toString() ?: "Failed to get RAM info")
|
||||||
|
sbc.getCpuUtilization { it ->
|
||||||
|
// result will come 1 second later, as array of pairs,
|
||||||
|
// with first being cpu name and second being utilization percentage
|
||||||
|
it.forEach { cpuinfo ->
|
||||||
|
cpustat[cpuinfo.first] = cpuinfo.second
|
||||||
|
}
|
||||||
|
//logger.info("CPU Utilization: ${cpustat.map { "${it.key.ifBlank { "avg" }}: ${ it.value}%" }}")
|
||||||
|
}
|
||||||
|
sbc.getNetworkTransferSpeed { it ->
|
||||||
|
// result will come 1 second later, as map of network interface name to Pair of RX and TX speeds
|
||||||
|
it.forEach { xx ->
|
||||||
|
networkstat[xx.key] = Pair(
|
||||||
|
netdev.toText(xx.value.first)+ "/s",
|
||||||
|
netdev.toText(xx.value.second) + "/s"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
//logger.info("Network Transfer Speed: ${networkstat.map { "${it.key}: RX=${it.value.key}, TX=${it.value.value}" }}")
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(5000) // Update every 5 seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var relay1 : DigitalOutput? = null
|
||||||
|
var relay2 : DigitalOutput? = null
|
||||||
|
var commandLED : DigitalOutput? = null
|
||||||
|
var streamingLED : DigitalOutput? = null
|
||||||
|
|
||||||
|
appscope.launch {
|
||||||
|
relay1 = DigitalOutput(NanopiDuo2.CLK.linuxGpio, "Relay1", true)
|
||||||
|
relay2 = DigitalOutput(NanopiDuo2.MISO.linuxGpio, "Relay2", true)
|
||||||
|
commandLED = DigitalOutput(NanopiDuo2.TX1.linuxGpio, "CommandLED", true)
|
||||||
|
streamingLED = DigitalOutput(NanopiDuo2.RX1.linuxGpio, "StreamingLED", true)
|
||||||
|
relay1.setOFF()
|
||||||
|
relay2.setOFF()
|
||||||
|
commandLED.setOFF()
|
||||||
|
streamingLED.setOFF()
|
||||||
|
}
|
||||||
|
|
||||||
var audioID = 0
|
var audioID = 0
|
||||||
val preferedAudioDevice = "Speakers"
|
val preferedAudioDevice = "USB Audio"
|
||||||
au.DetectPlaybackDevices().forEach { pair ->
|
AudioUtility.LoadLibraries()
|
||||||
println("Device ID: ${pair.first}, Name: ${pair.second}")
|
AudioUtility.PrintVersion()
|
||||||
|
|
||||||
|
AudioUtility.DetectPlaybackDevices().forEach { pair ->
|
||||||
|
logger.info("Device ID: ${pair.first}, Name: ${pair.second}")
|
||||||
if (pair.second.contains(preferedAudioDevice)) {
|
if (pair.second.contains(preferedAudioDevice)) {
|
||||||
audioID = pair.first
|
audioID = pair.first
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (audioID!=0){
|
if (audioID==0) audioID = 1 // fallback to first device if preferred not found
|
||||||
val initsuccess = au.InitDevice(audioID)
|
val initsuccess = AudioUtility.InitDevice(audioID,44100)
|
||||||
println("Audio Device $audioID initialized: $initsuccess")
|
logger.info("Audio Device $audioID initialized: $initsuccess")
|
||||||
}
|
|
||||||
|
|
||||||
|
// for Zello Client
|
||||||
val o = OpusStreamReceiver(audioID)
|
val o = OpusStreamReceiver(audioID)
|
||||||
|
// for AudioFilePlayer
|
||||||
var afp: AudioFilePlayer? = null
|
var afp: AudioFilePlayer? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a ZelloClient instance based on the configuration.
|
||||||
|
*/
|
||||||
|
fun CreateZelloFromConfig(): ZelloClient {
|
||||||
|
return when (cfg.ZelloServer) {
|
||||||
|
"work" -> {
|
||||||
|
ZelloClient.fromZelloWork(
|
||||||
|
cfg.ZelloUsername ?: "",
|
||||||
|
cfg.ZelloPassword ?: "",
|
||||||
|
cfg.ZelloChannel ?: "",
|
||||||
|
cfg.ZelloWorkNetworkName ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"enterprise" -> {
|
||||||
|
ZelloClient.fromZelloEnterpriseServer(
|
||||||
|
cfg.ZelloUsername ?: "",
|
||||||
|
cfg.ZelloPassword ?: "",
|
||||||
|
cfg.ZelloChannel ?: "",
|
||||||
|
cfg.ZelloEnterpriseServerDomain ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
ZelloClient.fromConsumerZello(cfg.ZelloUsername ?: "", cfg.ZelloPassword ?: "", cfg.ZelloChannel ?: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Zello client from configuration and start it
|
||||||
|
var z = CreateZelloFromConfig()
|
||||||
|
|
||||||
|
// Create ZelloEvent implementation to handle events
|
||||||
|
val z_event = object : ZelloEvent{
|
||||||
|
override fun onChannelStatus(
|
||||||
|
channel: String,
|
||||||
|
status: String,
|
||||||
|
userOnline: Int,
|
||||||
|
error: String?,
|
||||||
|
errorType: String?
|
||||||
|
) {
|
||||||
|
logger.info("Channel Status: $channel is $status with $userOnline users online.")
|
||||||
|
if (ValidString(error) && ValidString(errorType)) {
|
||||||
|
logger.info("Error: $error, Type: $errorType")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAudioData(streamID: Int, from: String, For: String, channel: String, data: ByteArray) {
|
||||||
|
logger.info("Audio Data received from $from for $For on channel $channel with streamID $streamID ")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onThumbnailImage(imageID: Int, from: String, For: String, channel: String, data: ByteArray, timestamp: Long) {
|
||||||
|
logger.info("Thumbnail Image received from $from for $For on channel $channel with imageID $imageID at timestamp $timestamp")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFullImage(imageID: Int, from: String, For: String, channel: String, data: ByteArray, timestamp: Long) {
|
||||||
|
logger.info("Full Image received from $from for $For on channel $channel with imageID $imageID at timestamp $timestamp")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTextMessage(messageID: Int, from: String, For: String, channel: String, text: String, timestamp: Long) {
|
||||||
|
logger.info("Text Message received from $from for $For on channel $channel with messageID $messageID at timestamp $timestamp. Text: $text")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLocation(messageID: Int, from: String, For: String, channel: String, latitude: Double, longitude: Double, address: String, accuracy: Double, timestamp: Long) {
|
||||||
|
logger.info("Location received from $from for $For on channel $channel with messageID $messageID at timestamp $timestamp. Location($latitude,$longitude), Address:$address, Accuracy:$accuracy")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConnected() {
|
||||||
|
logger.info("Connected to Zello server.")
|
||||||
|
relay1?.setOFF()
|
||||||
|
relay2?.setOFF()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDisconnected(reason: String) {
|
||||||
|
logger.info("Disconnected from Zello Server, reason: $reason")
|
||||||
|
logger.info("Reconnecting after 10 seconds...")
|
||||||
|
z.Stop()
|
||||||
|
relay1?.setOFF()
|
||||||
|
relay2?.setOFF()
|
||||||
|
val e = this
|
||||||
|
|
||||||
|
appscope.launch {
|
||||||
|
delay(10000) // Wait for 10 seconds before trying to reconnect
|
||||||
|
z = CreateZelloFromConfig()
|
||||||
|
z.Start(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(errorMessage: String) {
|
||||||
|
logger.info("Error occurred in Zello client: $errorMessage")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartStreaming(from: String, For: String, channel: String) {
|
||||||
|
// stop any previous playback
|
||||||
|
afp?.Stop()
|
||||||
|
afp = null
|
||||||
|
commandLED?.Blink()
|
||||||
|
if (o.Start()){
|
||||||
|
relay1?.setON()
|
||||||
|
relay2?.setON()
|
||||||
|
logger.info("Opus Receiver ready for streaming from $from for $For on channel $channel")
|
||||||
|
} else {
|
||||||
|
logger.info("Failed to start Opus Receiver for streaming from $from for $For on channel $channel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopStreaming(from: String, For: String, channel: String) {
|
||||||
|
o.Stop()
|
||||||
|
relay1?.setON()
|
||||||
|
relay2?.setOFF()
|
||||||
|
commandLED?.Blink()
|
||||||
|
logger.info("Opus Receiver stopped streaming from $from for $For on channel $channel")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStreamingData(
|
||||||
|
from: String,
|
||||||
|
For: String,
|
||||||
|
channel: String,
|
||||||
|
data: ByteArray
|
||||||
|
) {
|
||||||
|
if (o.isPlaying) o.PushData(data)
|
||||||
|
streamingLED?.Blink()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
z.Start(z_event)
|
||||||
|
|
||||||
|
// Start the web application with WebSocket support
|
||||||
val w = webApp("0.0.0.0",3030, BiFunction {
|
val w = webApp("0.0.0.0",3030, BiFunction {
|
||||||
source: String, cmd: WsCommand ->
|
source: String, cmd: WsCommand ->
|
||||||
when (source) {
|
when (source) {
|
||||||
"setting" -> when(cmd.command){
|
"setting" -> when(cmd.command){
|
||||||
"getConfig" ->{
|
"getConfig" ->{
|
||||||
logger.info("Get Config")
|
commandLED?.Blink()
|
||||||
WsReply(cmd.command,objectMapper.writeValueAsString(cfg) )
|
val data = mapOf(
|
||||||
|
"zelloUsername" to cfg.ZelloUsername,
|
||||||
|
"zelloPassword" to cfg.ZelloPassword,
|
||||||
|
"zelloChannel" to cfg.ZelloChannel,
|
||||||
|
"zelloServer" to cfg.ZelloServer,
|
||||||
|
"zelloWorkNetworkName" to cfg.ZelloWorkNetworkName,
|
||||||
|
"zelloEnterpriseServerDomain" to cfg.ZelloEnterpriseServerDomain,
|
||||||
|
"m1" to cfg.M1,
|
||||||
|
"m2" to cfg.M2,
|
||||||
|
"m3" to cfg.M3,
|
||||||
|
"m4" to cfg.M4,
|
||||||
|
"m5" to cfg.M5,
|
||||||
|
"m6" to cfg.M6,
|
||||||
|
"m7" to cfg.M7,
|
||||||
|
"m8" to cfg.M8,
|
||||||
|
"messageList" to Codes.getAudioFiles()
|
||||||
|
)
|
||||||
|
|
||||||
|
WsReply(cmd.command, objectMapper.writeValueAsString(data).trim())
|
||||||
}
|
}
|
||||||
"setZelloConfig" -> {
|
"setZelloConfig" -> {
|
||||||
|
commandLED?.Blink()
|
||||||
try{
|
try{
|
||||||
val xx = objectMapper.readValue(cmd.data, object: TypeReference<Map<String, String>>() {})
|
val xx = objectMapper.readValue(cmd.data, object: TypeReference<Map<String, String>>() {})
|
||||||
cfg.ZelloUsername = xx["ZelloUsername"]
|
var changed = false
|
||||||
cfg.ZelloPassword = xx["ZelloPassword"]
|
if (cfg.ZelloUsername != xx["ZelloUsername"]) {
|
||||||
cfg.ZelloChannel = xx["ZelloChannel"]
|
cfg.ZelloUsername = xx["ZelloUsername"] ?: ""
|
||||||
cfg.ZelloServer = xx["ZelloServer"]
|
changed = true
|
||||||
cfg.ZelloWorkNetworkName = xx["ZelloWorkNetworkName"]
|
}
|
||||||
cfg.ZelloEnterpriseServerDomain = xx["ZelloEnterpriseServerDomain"]
|
if (cfg.ZelloPassword != xx["ZelloPassword"]) {
|
||||||
|
cfg.ZelloPassword = xx["ZelloPassword"] ?: ""
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if (cfg.ZelloChannel != xx["ZelloChannel"]) {
|
||||||
|
cfg.ZelloChannel = xx["ZelloChannel"] ?: ""
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if (cfg.ZelloServer != xx["ZelloServer"]) {
|
||||||
|
cfg.ZelloServer = xx["ZelloServer"] ?: ""
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if (cfg.ZelloWorkNetworkName != xx["ZelloWorkNetworkName"]) {
|
||||||
|
cfg.ZelloWorkNetworkName = xx["ZelloWorkNetworkName"] ?: ""
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if (cfg.ZelloEnterpriseServerDomain != xx["ZelloEnterpriseServerDomain"]) {
|
||||||
|
cfg.ZelloEnterpriseServerDomain = xx["ZelloEnterpriseServerDomain"] ?: ""
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if (changed){
|
||||||
|
cfg.Save()
|
||||||
|
z.Stop()
|
||||||
|
z = CreateZelloFromConfig()
|
||||||
|
z.Start(z_event)
|
||||||
|
|
||||||
|
|
||||||
WsReply(cmd.command,"success")
|
WsReply(cmd.command,"success")
|
||||||
|
} else WsReply(cmd.command,"No changes made")
|
||||||
} catch (e: Exception){
|
} catch (e: Exception){
|
||||||
WsReply(cmd.command,"failed: ${e.message}")
|
WsReply(cmd.command,"failed: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
"setMessageConfig"-> {
|
"setMessageConfig"-> {
|
||||||
|
commandLED?.Blink()
|
||||||
try{
|
try{
|
||||||
|
|
||||||
val xx = objectMapper.readValue(cmd.data, object : TypeReference<Map<String, String>>() {})
|
val xx = objectMapper.readValue(cmd.data, object : TypeReference<Map<String, String>>() {})
|
||||||
cfg.M1 = xx["M1"]
|
var changed = false
|
||||||
cfg.M2 = xx["M2"]
|
if (cfg.M1 != xx["M1"]) {
|
||||||
cfg.M3 = xx["M3"]
|
cfg.M1 = xx["M1"] ?: ""
|
||||||
cfg.M4 = xx["M4"]
|
changed = true
|
||||||
cfg.M5 = xx["M5"]
|
}
|
||||||
cfg.M6 = xx["M6"]
|
if (cfg.M2 != xx["M2"]) {
|
||||||
cfg.M7 = xx["M7"]
|
cfg.M2 = xx["M2"] ?: ""
|
||||||
cfg.M8 = xx["M8"]
|
changed = true
|
||||||
|
}
|
||||||
|
if (cfg.M3 != xx["M3"]) {
|
||||||
|
cfg.M3 = xx["M3"] ?: ""
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if (cfg.M4 != xx["M4"]) {
|
||||||
|
cfg.M4 = xx["M4"] ?: ""
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if (cfg.M5 != xx["M5"]) {
|
||||||
|
cfg.M5 = xx["M5"] ?: ""
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if (cfg.M6 != xx["M6"]) {
|
||||||
|
cfg.M6 = xx["M6"] ?: ""
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if (cfg.M7 != xx["M7"]) {
|
||||||
|
cfg.M7 = xx["M7"] ?: ""
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if (cfg.M8 != xx["M8"]) {
|
||||||
|
cfg.M8 = xx["M8"] ?: ""
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if (changed){
|
||||||
|
cfg.Save()
|
||||||
WsReply(cmd.command,"success")
|
WsReply(cmd.command,"success")
|
||||||
|
} else WsReply(cmd.command,"No changes made")
|
||||||
} catch (e: Exception){
|
} catch (e: Exception){
|
||||||
WsReply(cmd.command,"failed: ${e.message}")
|
WsReply(cmd.command,"failed: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -87,6 +393,7 @@ fun main() {
|
|||||||
|
|
||||||
"prerecordedbroadcast" -> when(cmd.command){
|
"prerecordedbroadcast" -> when(cmd.command){
|
||||||
"getMessageConfig" ->{
|
"getMessageConfig" ->{
|
||||||
|
commandLED?.Blink()
|
||||||
val data = mapOf(
|
val data = mapOf(
|
||||||
"M1" to cfg.M1,
|
"M1" to cfg.M1,
|
||||||
"M2" to cfg.M2,
|
"M2" to cfg.M2,
|
||||||
@@ -100,13 +407,20 @@ fun main() {
|
|||||||
WsReply(cmd.command, objectMapper.writeValueAsString(data))
|
WsReply(cmd.command, objectMapper.writeValueAsString(data))
|
||||||
}
|
}
|
||||||
"getPlaybackStatus" ->{
|
"getPlaybackStatus" ->{
|
||||||
|
commandLED?.Blink()
|
||||||
if (afp!=null && true==afp?.isPlaying){
|
if (afp!=null && true==afp?.isPlaying){
|
||||||
WsReply(cmd.command, "Playing: ${afp?.filename}")
|
WsReply(cmd.command, "Playing: ${afp?.filename}, Duration: ${afp?.duration?.toInt()}, Elapsed: ${afp?.elapsed?.toInt()} seconds")
|
||||||
} else {
|
} else {
|
||||||
WsReply(cmd.command, "Idle")
|
WsReply(cmd.command, "Idle")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"playMessage" ->{
|
"playMessage" ->{
|
||||||
|
commandLED?.Blink()
|
||||||
|
afp?.Stop()
|
||||||
|
afp = null
|
||||||
|
// stop Opus Receiver if it is running
|
||||||
|
o.Stop()
|
||||||
|
|
||||||
val filename = when(cmd.data){
|
val filename = when(cmd.data){
|
||||||
"M1" -> cfg.M1
|
"M1" -> cfg.M1
|
||||||
"M2" -> cfg.M2
|
"M2" -> cfg.M2
|
||||||
@@ -120,29 +434,55 @@ fun main() {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ValidFile(filename)){
|
relay1?.setOFF()
|
||||||
|
relay2?.setOFF()
|
||||||
|
if (filename!=null){
|
||||||
try{
|
try{
|
||||||
val player= AudioFilePlayer(audioID, filename)
|
afp= AudioFilePlayer(audioID, filename)
|
||||||
player.Play { _ -> afp = null}
|
afp?.Play { _ ->
|
||||||
afp = player
|
afp = null
|
||||||
|
relay1?.setOFF()
|
||||||
|
relay2?.setOFF()
|
||||||
|
}
|
||||||
|
relay1?.setON()
|
||||||
|
relay2?.setON()
|
||||||
WsReply(cmd.command,"success")
|
WsReply(cmd.command,"success")
|
||||||
|
|
||||||
} catch (e: Exception){
|
} catch (e: Exception){
|
||||||
|
afp?.Stop()
|
||||||
|
|
||||||
|
afp = null
|
||||||
WsReply(cmd.command, "failed: ${e.message}")
|
WsReply(cmd.command, "failed: ${e.message}")
|
||||||
}
|
}
|
||||||
} else WsReply(cmd.command,"Invalid file : $filename")
|
} else {
|
||||||
|
afp?.Stop()
|
||||||
|
afp = null
|
||||||
|
WsReply(cmd.command,"Invalid message name: ${cmd.data}")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
"stopMessage" ->{
|
"stopMessage" ->{
|
||||||
|
commandLED?.Blink()
|
||||||
afp?.Stop()
|
afp?.Stop()
|
||||||
afp = null
|
afp = null
|
||||||
|
relay1?.setOFF()
|
||||||
|
relay2?.setOFF()
|
||||||
WsReply(cmd.command,"success")
|
WsReply(cmd.command,"success")
|
||||||
}
|
}
|
||||||
else -> WsReply(cmd.command,"Invalid command: ${cmd.command}")
|
else -> WsReply(cmd.command,"Invalid command: ${cmd.command}")
|
||||||
}
|
}
|
||||||
"pocreceiver" -> when(cmd.command){
|
"pocreceiver" -> when(cmd.command){
|
||||||
|
"getZelloStatus" -> {
|
||||||
|
commandLED?.Blink()
|
||||||
|
var status = "Disconnected"
|
||||||
|
if (z.currentChannel?.isNotBlank() == true){
|
||||||
|
status = "Channel: ${z.currentChannel}, Online: ${z.isOnline}, Username: ${z.username}"
|
||||||
|
if (z.isReceivingStreaming){
|
||||||
|
status += ", Streaming From: ${z.receivingFrom}, Bytes Received: ${Codes.SizeToString(z.bytesReceived)}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WsReply(cmd.command, status)
|
||||||
|
}
|
||||||
else -> WsReply(cmd.command,"Invalid command: ${cmd.command}")
|
else -> WsReply(cmd.command,"Invalid command: ${cmd.command}")
|
||||||
}
|
}
|
||||||
else -> WsReply(cmd.command,"Invalid source: $source")
|
else -> WsReply(cmd.command,"Invalid source: $source")
|
||||||
@@ -151,76 +491,11 @@ fun main() {
|
|||||||
} , Pair("admin","admin1234"))
|
} , Pair("admin","admin1234"))
|
||||||
w.Start()
|
w.Start()
|
||||||
|
|
||||||
val z = ZelloClient.fromConsumerZello("gtcdevice01","GtcDev2025")
|
|
||||||
z.Start(object : ZelloEvent {
|
|
||||||
override fun onChannelStatus(
|
|
||||||
channel: String,
|
|
||||||
status: String,
|
|
||||||
userOnline: Int,
|
|
||||||
error: String?,
|
|
||||||
errorType: String?
|
|
||||||
) {
|
|
||||||
println("Channel Status: $channel is $status with $userOnline users online.")
|
|
||||||
if (ValidString(error) && ValidString(errorType)) {
|
|
||||||
println("Error: $error, Type: $errorType")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAudioData(streamID: Int, from: String, For: String, channel: String, data: ByteArray) {
|
|
||||||
println("Audio Data received from $from for $For on channel $channel with streamID $streamID ")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onThumbnailImage(imageID: Int, from: String, For: String, channel: String, data: ByteArray, timestamp: Long) {
|
|
||||||
println("Thumbnail Image received from $from for $For on channel $channel with imageID $imageID at timestamp $timestamp")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFullImage(imageID: Int, from: String, For: String, channel: String, data: ByteArray, timestamp: Long) {
|
|
||||||
println("Full Image received from $from for $For on channel $channel with imageID $imageID at timestamp $timestamp")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTextMessage(messageID: Int, from: String, For: String, channel: String, text: String, timestamp: Long) {
|
|
||||||
println("Text Message received from $from for $For on channel $channel with messageID $messageID at timestamp $timestamp. Text: $text")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLocation(messageID: Int, from: String, For: String, channel: String, latitude: Double, longitude: Double, address: String, accuracy: Double, timestamp: Long) {
|
|
||||||
println("Location received from $from for $For on channel $channel with messageID $messageID at timestamp $timestamp. Location($latitude,$longitude), Address:$address, Accuracy:$accuracy")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConnected() {
|
|
||||||
println("Connected to Zello server.")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDisconnected() {
|
|
||||||
println("Disconnected from Zello server.")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(errorMessage: String) {
|
|
||||||
println("Error occurred in Zello client: $errorMessage")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartStreaming(from: String, For: String, channel: String) {
|
|
||||||
if (o.Start()){
|
|
||||||
println("Opus Receiver ready for streaming from $from for $For on channel $channel")
|
|
||||||
} else {
|
|
||||||
println("Failed to start Opus Receiver for streaming from $from for $For on channel $channel")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStopStreaming(from: String, For: String, channel: String) {
|
|
||||||
o.Stop()
|
|
||||||
println("Opus Receiver stopped streaming from $from for $For on channel $channel")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStreamingData(
|
|
||||||
from: String,
|
|
||||||
For: String,
|
|
||||||
channel: String,
|
|
||||||
data: ByteArray
|
|
||||||
) {
|
|
||||||
if (o.isPlaying) o.PushData(data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,74 +1,73 @@
|
|||||||
package audio
|
package audio
|
||||||
|
|
||||||
|
import audio.AudioUtility.Companion.bass
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import somecodes.Codes
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Audio Player for playing audio files.
|
* Audio Player for playing audio files.
|
||||||
* Supported extensions : .wav, .mp3
|
* Supported extensions : .wav, .mp3
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
class AudioFilePlayer(deviceID: Int, val filename: String?, device_samplingrate: Int = 48000) {
|
class AudioFilePlayer(deviceID: Int, val filename: String, device_samplingrate: Int = 48000) {
|
||||||
val bass: Bass = Bass.Instance
|
|
||||||
var filehandle = 0
|
private var filehandle = 0
|
||||||
var isPlaying = false
|
var isPlaying = false
|
||||||
|
val fileSize: Long
|
||||||
|
val duration: Double
|
||||||
|
var elapsed: Double = 0.0
|
||||||
init{
|
init{
|
||||||
if (bass.BASS_SetDevice(deviceID)){
|
val fullpath = Codes.audioFilePath.resolve(filename)
|
||||||
filehandle = bass.BASS_StreamCreateFile(false, filename, 0, 0, 0)
|
if (fullpath.exists()){
|
||||||
if (filehandle == 0) {
|
if (AudioUtility.InitDevice(deviceID, device_samplingrate)) {
|
||||||
throw Exception("Failed to create stream for file $filename: ${bass.BASS_ErrorGetCode()}")
|
AudioUtility.setVolumeOutput(deviceID,1.0f)
|
||||||
}
|
filehandle = bass.BASS_StreamCreateFile(false, fullpath.absolutePathString(), 0, 0, 0)
|
||||||
} else throw Exception("Failed to set device $deviceID")
|
if (filehandle!=0){
|
||||||
|
fileSize = bass.BASS_ChannelGetLength(filehandle, Bass.BASS_POS_BYTE)
|
||||||
|
duration = bass.BASS_ChannelBytes2Seconds(filehandle, fileSize)
|
||||||
|
} else throw Exception("Failed to create stream for file $filename: ${bass.BASS_ErrorGetCode()}")
|
||||||
|
|
||||||
|
} else throw Exception("Error initializing device $deviceID with sampling rate $device_samplingrate, code ${bass.BASS_ErrorGetCode()}")
|
||||||
|
} else throw Exception("File $filename does not exists")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Stop(){
|
fun Stop(){
|
||||||
if (filehandle!=0){
|
if (filehandle!=0){
|
||||||
bass.BASS_ChannelStop(filehandle)
|
bass.BASS_ChannelFree(filehandle)
|
||||||
bass.BASS_StreamFree(filehandle)
|
|
||||||
filehandle = 0
|
filehandle = 0
|
||||||
}
|
}
|
||||||
|
isPlaying = false
|
||||||
|
AudioUtility.Free()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Play(finished: Consumer<Any> ) : Boolean{
|
fun Play(finished: Consumer<Any> ) : Boolean{
|
||||||
if (bass.BASS_ChannelPlay(filehandle, false)){
|
if (bass.BASS_ChannelPlay(filehandle, false)){
|
||||||
|
bass.BASS_ChannelSetAttribute(filehandle, Bass.BASS_ATTRIB_VOL, 1.0f)
|
||||||
|
elapsed = 0.0
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
isPlaying = true
|
isPlaying = true
|
||||||
while(true){
|
while(true){
|
||||||
delay(1000)
|
delay(50)
|
||||||
|
|
||||||
if (bass.BASS_ChannelIsActive(filehandle)!= Bass.BASS_ACTIVE_PLAYING){
|
if (bass.BASS_ChannelIsActive(filehandle)!= Bass.BASS_ACTIVE_PLAYING){
|
||||||
// finished playing
|
// finished playing
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
elapsed = bass.BASS_ChannelBytes2Seconds(filehandle, bass.BASS_ChannelGetPosition(filehandle, Bass.BASS_POS_BYTE))
|
||||||
}
|
}
|
||||||
isPlaying = false
|
isPlaying = false
|
||||||
bass.BASS_StreamFree(filehandle)
|
bass.BASS_ChannelFree(filehandle)
|
||||||
filehandle = 0
|
filehandle = 0
|
||||||
finished.accept(true)
|
finished.accept(true)
|
||||||
}
|
}
|
||||||
// Revisi 06/08/2025 Ganti thread dengan Coroutine
|
|
||||||
// val thread = Thread{
|
|
||||||
// isPlaying = true
|
|
||||||
// while(true){
|
|
||||||
// Thread.sleep(1000)
|
|
||||||
//
|
|
||||||
// if (bass.BASS_ChannelIsActive(filehandle)!= Bass.BASS_ACTIVE_PLAYING){
|
|
||||||
// // finished playing
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// isPlaying = false
|
|
||||||
// bass.BASS_StreamFree(filehandle)
|
|
||||||
// filehandle = 0
|
|
||||||
// finished.accept(true)
|
|
||||||
// }
|
|
||||||
// thread.name = "AudioFilePlayer $filename"
|
|
||||||
// thread.isDaemon = true
|
|
||||||
// thread.start()
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -1,13 +1,62 @@
|
|||||||
package audio
|
package audio
|
||||||
|
import com.sun.jna.Platform
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import kotlin.io.path.isRegularFile
|
||||||
|
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
class AudioUtility {
|
class AudioUtility {
|
||||||
private val bass = Bass.Instance
|
companion object{
|
||||||
|
val bass : Bass = Bass.Instance
|
||||||
private val logger : Logger = LoggerFactory.getLogger(AudioUtility::class.java)
|
private val logger : Logger = LoggerFactory.getLogger(AudioUtility::class.java)
|
||||||
|
|
||||||
init{
|
fun ExtractLibraries(parent: String, filename: String, targetPath: Path) : Path?{
|
||||||
logger.info("Bass Version = ${bass.BASS_GetVersion().toHexString()}")
|
if (targetPath.resolve(filename).isRegularFile()) return targetPath.resolve(filename) // already exists
|
||||||
|
val out = targetPath.resolve(filename)
|
||||||
|
|
||||||
|
try{
|
||||||
|
AudioUtility::class.java.getResourceAsStream("$parent/$filename").use { ins ->
|
||||||
|
requireNotNull(ins) { "Resource $parent/$filename not found" }
|
||||||
|
Files.copy(ins, out, REPLACE_EXISTING)
|
||||||
|
}
|
||||||
|
out.toFile().setReadable(true, false)
|
||||||
|
out.toFile().setExecutable(true, false)
|
||||||
|
return out
|
||||||
|
} catch (_: Exception){
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LoadLibraries(){
|
||||||
|
val runfolder = Path.of(System.getProperty("user.dir"))
|
||||||
|
//logger.info("Checking for BASS libraries in $runfolder")
|
||||||
|
val resourceprefix = Platform.RESOURCE_PREFIX
|
||||||
|
logger.info("Resource prefix : $resourceprefix")
|
||||||
|
val bass = ExtractLibraries(resourceprefix, System.mapLibraryName("bass"), runfolder)
|
||||||
|
val bassopus = ExtractLibraries(resourceprefix, System.mapLibraryName("bassopus"), runfolder)
|
||||||
|
if (bass!=null){
|
||||||
|
System.load(bass.toString())
|
||||||
|
}
|
||||||
|
if (bassopus!=null){
|
||||||
|
System.load(bassopus.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PrintVersion(){
|
||||||
|
logger.info("BASS Version: ${bass.BASS_GetVersion().toHexString()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LoadPlugin(plugin: String){
|
||||||
|
if (bass.BASS_PluginLoad(plugin, 0)>0){
|
||||||
|
logger.info("Plugin $plugin loaded successfully")
|
||||||
|
} else {
|
||||||
|
logger.error("Failed to load plugin $plugin: ${bass.BASS_ErrorGetCode()}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun DetectPlaybackDevices() : List<Pair<Int, String>> {
|
fun DetectPlaybackDevices() : List<Pair<Int, String>> {
|
||||||
@@ -23,6 +72,14 @@ class AudioUtility {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setVolumeOutput(deviceID: Int, value: Float){
|
||||||
|
var vol = value
|
||||||
|
if (vol < 0) vol = 0f
|
||||||
|
if (vol > 1) vol = 1f
|
||||||
|
bass.BASS_SetDevice(deviceID)
|
||||||
|
bass.BASS_SetVolume(vol)
|
||||||
|
}
|
||||||
|
|
||||||
fun InitDevice(deviceID: Int, device_samplingrate: Int = 48000) : Boolean {
|
fun InitDevice(deviceID: Int, device_samplingrate: Int = 48000) : Boolean {
|
||||||
val dev = Bass.BASS_DEVICEINFO()
|
val dev = Bass.BASS_DEVICEINFO()
|
||||||
if (bass.BASS_GetDeviceInfo(deviceID, dev)) {
|
if (bass.BASS_GetDeviceInfo(deviceID, dev)) {
|
||||||
@@ -38,4 +95,23 @@ class AudioUtility {
|
|||||||
return false // gagal GetDeviceInfo
|
return false // gagal GetDeviceInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the device is having some handles opened.
|
||||||
|
* @param deviceID the device ID to check
|
||||||
|
* @return true if the device is opening something, false otherwise
|
||||||
|
*/
|
||||||
|
fun DeviceIsOpeningSomething(deviceID: Int) : Boolean {
|
||||||
|
return bass.BASS_GetConfig(Bass.BASS_CONFIG_HANDLES) > 0
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Free(){
|
||||||
|
bass.BASS_Free()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package audio
|
package audio
|
||||||
|
|
||||||
|
import audio.AudioUtility.Companion.bass
|
||||||
import com.sun.jna.Memory
|
import com.sun.jna.Memory
|
||||||
import com.sun.jna.Pointer
|
import com.sun.jna.Pointer
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
@@ -12,37 +13,40 @@ import org.slf4j.LoggerFactory
|
|||||||
* @throws Exception if the device cannot be set.
|
* @throws Exception if the device cannot be set.
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
class OpusStreamReceiver(deviceID: Int, val samplingrate: Int = 16000) {
|
class OpusStreamReceiver(val deviceID: Int, val samplingrate: Int = 16000) {
|
||||||
private val bass = Bass.Instance
|
|
||||||
private val bassopus = BASSOPUS.Instance
|
|
||||||
private var filehandle = 0
|
private var filehandle = 0
|
||||||
private val logger : Logger = LoggerFactory.getLogger(OpusStreamReceiver::class.java)
|
private val logger : Logger = LoggerFactory.getLogger(OpusStreamReceiver::class.java)
|
||||||
var isPlaying = false
|
var isPlaying = false
|
||||||
|
val bassopus : BASSOPUS = BASSOPUS.Instance
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
init{
|
|
||||||
if (!bass.BASS_SetDevice(deviceID)){
|
|
||||||
throw Exception("Failed to set device $deviceID")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the Opus stream playback.
|
* Starts the Opus stream playback.
|
||||||
* @return true if the stream started successfully, false otherwise.
|
* @return true if the stream started successfully, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun Start() : Boolean{
|
fun Start() : Boolean{
|
||||||
|
|
||||||
|
if (AudioUtility.InitDevice(deviceID, samplingrate)){
|
||||||
val opushead = BASSOPUS.BASS_OPUS_HEAD()
|
val opushead = BASSOPUS.BASS_OPUS_HEAD()
|
||||||
opushead.version = 1
|
opushead.version = 1
|
||||||
opushead.channels = 1
|
opushead.channels = 1
|
||||||
opushead.inputrate = samplingrate
|
opushead.inputrate = samplingrate
|
||||||
val procpush = Pointer(-1)
|
val procpush = Pointer(-1)
|
||||||
|
AudioUtility.setVolumeOutput(deviceID, 1.0f)
|
||||||
|
|
||||||
filehandle = bassopus.BASS_OPUS_StreamCreate(opushead,0, procpush, null)
|
filehandle = bassopus.BASS_OPUS_StreamCreate(opushead,0, procpush, null)
|
||||||
if (filehandle != 0){
|
if (filehandle != 0){
|
||||||
if (bass.BASS_ChannelPlay(filehandle,false)){
|
if (bass.BASS_ChannelPlay(filehandle,false)){
|
||||||
|
bass.BASS_ChannelSetAttribute(filehandle, Bass.BASS_ATTRIB_VOL, 1.0f)
|
||||||
isPlaying = true
|
isPlaying = true
|
||||||
return true
|
return true
|
||||||
} else logger.error("BASS_ChannelPlay failed for filehandle $filehandle, code ${bass.BASS_ErrorGetCode()}")
|
} else logger.error("BASS_ChannelPlay failed for filehandle $filehandle, code ${bass.BASS_ErrorGetCode()}")
|
||||||
} else logger.error("BASS_OPUS_StreamCreate failed, code ${bass.BASS_ErrorGetCode()}")
|
} else logger.error("BASS_OPUS_StreamCreate failed, code ${bass.BASS_ErrorGetCode()}")
|
||||||
|
} else logger.error("Error initializing device $deviceID with sampling rate $samplingrate, code ${bass.BASS_ErrorGetCode()}")
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,11 +55,11 @@ class OpusStreamReceiver(deviceID: Int, val samplingrate: Int = 16000) {
|
|||||||
*/
|
*/
|
||||||
fun Stop(){
|
fun Stop(){
|
||||||
if (filehandle!=0){
|
if (filehandle!=0){
|
||||||
bass.BASS_ChannelStop(filehandle)
|
bass.BASS_ChannelFree(filehandle)
|
||||||
bass.BASS_StreamFree(filehandle)
|
|
||||||
filehandle = 0
|
filehandle = 0
|
||||||
isPlaying = false
|
|
||||||
}
|
}
|
||||||
|
isPlaying = false
|
||||||
|
AudioUtility.Free()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
157
src/sbc/DigitalOutput.kt
Normal file
157
src/sbc/DigitalOutput.kt
Normal 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
21
src/sbc/NanopiDuo2.kt
Normal 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
39
src/sbc/RaspberryPi4.kt
Normal 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
300
src/sbc/SbcInfo.kt
Normal 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
3
src/sbc/cpuinfo.kt
Normal 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
49
src/sbc/cpustat.kt
Normal 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
5
src/sbc/devstat.kt
Normal 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
76
src/sbc/meminfo.kt
Normal 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
28
src/sbc/netdev.kt
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,12 +1,64 @@
|
|||||||
package somecodes
|
package somecodes
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.sun.jna.Platform
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.nio.file.Path
|
||||||
|
import kotlin.io.path.Path
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
class Codes {
|
class Codes {
|
||||||
private val objectMapper = ObjectMapper()
|
|
||||||
companion object{
|
companion object{
|
||||||
|
val audioFilePath = Path(System.getProperty("user.dir"), "audiofile")
|
||||||
|
val gpioPath : Path = Path("/sys/class/gpio")
|
||||||
|
val gpioExportPath : Path = gpioPath.resolve("export")
|
||||||
|
val gpioUnexportPath : Path = gpioPath.resolve("unexport")
|
||||||
|
private val logger = LoggerFactory.getLogger("Codes")
|
||||||
|
private val validAudioExtensions = setOf("wav", "mp3")
|
||||||
|
private const val KB_size = 1024.0 // 1 KB = 1024 bytes
|
||||||
|
private const val MB_size = 1024.0 * KB_size // 1 MB = 1024 KB
|
||||||
|
private const val GB_size = 1024.0 * MB_size // 1 GB = 1024 MB
|
||||||
|
|
||||||
|
fun isLinux() : Boolean {
|
||||||
|
return Platform.isLinux()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun haveGpioSupport() : Boolean {
|
||||||
|
if (Platform.isLinux()){
|
||||||
|
if (gpioExportPath.toFile().exists() && gpioUnexportPath.toFile().exists()) {
|
||||||
|
if (gpioExportPath.toFile().canWrite() && gpioUnexportPath.toFile().canWrite()) {
|
||||||
|
return true
|
||||||
|
} else logger.error("$gpioExportPath or $gpioUnexportPath is not writable")
|
||||||
|
} else logger.error("$gpioExportPath or $gpioUnexportPath does not exist")
|
||||||
|
} else logger.error("Platform is not Linux, GPIO support is not available")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SizeToString(size: Long) : String {
|
||||||
|
return when {
|
||||||
|
size >= GB_size -> String.format("%.2f GB", size / GB_size)
|
||||||
|
size >= MB_size -> String.format("%.2f MB", size / MB_size)
|
||||||
|
size >= KB_size -> String.format("%.2f KB", size / KB_size)
|
||||||
|
else -> "$size bytes"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun getAudioFiles() : Array<String> {
|
||||||
|
val audioDir = audioFilePath.toFile()
|
||||||
|
if (!audioDir.exists()) {
|
||||||
|
audioDir.mkdirs() // Create directory if it doesn't exist
|
||||||
|
}
|
||||||
|
val ll = audioDir.listFiles()?.filter { it.isFile && validAudioExtensions.contains(it.extension) }?.map { it.name } ?: emptyList()
|
||||||
|
return ll.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getaudioFileFullPath(filename: String) : String {
|
||||||
|
if (ValidString(filename)) {
|
||||||
|
val file = audioFilePath.resolve(filename)
|
||||||
|
return file.toAbsolutePath().toString()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
fun ValidFile(s: String?) : Boolean {
|
fun ValidFile(s: String?) : Boolean {
|
||||||
if (s!=null){
|
if (s!=null){
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package somecodes
|
package somecodes
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import kotlin.io.path.Path
|
import kotlin.io.path.Path
|
||||||
|
|
||||||
@@ -21,15 +22,14 @@ class configFile {
|
|||||||
|
|
||||||
private val filepath : Path = Path(System.getProperty("user.dir"), "config.json")
|
private val filepath : Path = Path(System.getProperty("user.dir"), "config.json")
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun Load(){
|
fun Load(){
|
||||||
if (filepath.toFile().exists()){
|
if (filepath.toFile().exists()){
|
||||||
// file found, then load the configuration to configFile object
|
// file found, then load the configuration to configFile object
|
||||||
try{
|
try{
|
||||||
val json = filepath.toFile().readText()
|
val json = filepath.toFile().readText()
|
||||||
val configMap = json.split(",").associate { it ->
|
val objectMapper = jacksonObjectMapper()
|
||||||
val (key, value) = it.split(":").map { it.trim().removeSurrounding("\"") }
|
val configMap = objectMapper.readValue(json, Map::class.java) as Map<String, String>
|
||||||
key to value
|
|
||||||
}
|
|
||||||
|
|
||||||
ZelloUsername = configMap["ZelloUsername"]
|
ZelloUsername = configMap["ZelloUsername"]
|
||||||
ZelloPassword = configMap["ZelloPassword"]
|
ZelloPassword = configMap["ZelloPassword"]
|
||||||
@@ -72,23 +72,25 @@ class configFile {
|
|||||||
fun Save(){
|
fun Save(){
|
||||||
try {
|
try {
|
||||||
// Convert the configFile object to JSON and write it to the file
|
// Convert the configFile object to JSON and write it to the file
|
||||||
val json = """{
|
val js = mapOf(
|
||||||
"ZelloUsername": "$ZelloUsername",
|
"ZelloUsername" to ZelloUsername,
|
||||||
"ZelloPassword": "$ZelloPassword",
|
"ZelloPassword" to ZelloPassword,
|
||||||
"ZelloChannel": "$ZelloChannel",
|
"ZelloChannel" to ZelloChannel,
|
||||||
"ZelloServer": "$ZelloServer",
|
"ZelloServer" to ZelloServer,
|
||||||
"ZelloWorkNetworkName": "$ZelloWorkNetworkName",
|
"ZelloWorkNetworkName" to ZelloWorkNetworkName,
|
||||||
"ZelloEnterpriseServerDomain": "$ZelloEnterpriseServerDomain",
|
"ZelloEnterpriseServerDomain" to ZelloEnterpriseServerDomain,
|
||||||
"M1": "$M1",
|
"M1" to M1,
|
||||||
"M2": "$M2",
|
"M2" to M2,
|
||||||
"M3": "$M3",
|
"M3" to M3,
|
||||||
"M4": "$M4",
|
"M4" to M4,
|
||||||
"M5": "$M5",
|
"M5" to M5,
|
||||||
"M6": "$M6",
|
"M6" to M6,
|
||||||
"M7": "$M7",
|
"M7" to M7,
|
||||||
"M8": "$M8"
|
"M8" to M8
|
||||||
}"""
|
)
|
||||||
filepath.toFile().writeText(json)
|
|
||||||
|
val mapper = jacksonObjectMapper()
|
||||||
|
mapper.writerWithDefaultPrettyPrinter().writeValue(filepath.toFile(), js)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Error saving configuration: ${e.message}")
|
println("Error saving configuration: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ package web;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.javalin.Javalin;
|
import io.javalin.Javalin;
|
||||||
|
import io.javalin.http.HttpStatus;
|
||||||
import javafx.util.Pair;
|
import javafx.util.Pair;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import somecodes.Codes;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
@@ -30,7 +35,7 @@ public class webApp {
|
|||||||
}
|
}
|
||||||
app = Javalin.create(config -> {
|
app = Javalin.create(config -> {
|
||||||
config.useVirtualThreads = true; // Enable virtual threads for better performance
|
config.useVirtualThreads = true; // Enable virtual threads for better performance
|
||||||
config.staticFiles.add("/");
|
config.staticFiles.add("/webpage");
|
||||||
config.router.apiBuilder(()->{
|
config.router.apiBuilder(()->{
|
||||||
path("/", () -> get(ctx -> {
|
path("/", () -> get(ctx -> {
|
||||||
if (ctx.sessionAttribute("user") == null) {
|
if (ctx.sessionAttribute("user") == null) {
|
||||||
@@ -66,7 +71,7 @@ public class webApp {
|
|||||||
String message = wsMessageContext.message();
|
String message = wsMessageContext.message();
|
||||||
try{
|
try{
|
||||||
var command = objectMapper.readValue(message, WsCommand.class);
|
var command = objectMapper.readValue(message, WsCommand.class);
|
||||||
logger.info("Received command from pocreceiver.html/ws : {}", command);
|
//logger.info("Received command from pocreceiver.html/ws : {}", command);
|
||||||
var reply = callback.apply("pocreceiver", command);
|
var reply = callback.apply("pocreceiver", command);
|
||||||
wsMessageContext.send(reply);
|
wsMessageContext.send(reply);
|
||||||
} catch (Exception e){
|
} catch (Exception e){
|
||||||
@@ -85,7 +90,7 @@ public class webApp {
|
|||||||
String message = wsMessageContext.message();
|
String message = wsMessageContext.message();
|
||||||
try{
|
try{
|
||||||
var command = objectMapper.readValue(message, WsCommand.class);
|
var command = objectMapper.readValue(message, WsCommand.class);
|
||||||
logger.info("Received command from prerecordedbroadcast.html/ws : {}", command);
|
//logger.info("Received command from prerecordedbroadcast.html/ws : {}", command);
|
||||||
var reply = callback.apply("prerecordedbroadcast", command);
|
var reply = callback.apply("prerecordedbroadcast", command);
|
||||||
wsMessageContext.send(reply);
|
wsMessageContext.send(reply);
|
||||||
} catch (Exception e){
|
} catch (Exception e){
|
||||||
@@ -99,14 +104,33 @@ public class webApp {
|
|||||||
ctx.redirect("/index.html");
|
ctx.redirect("/index.html");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
post("/upload", ctx -> {
|
||||||
|
// Handle file upload
|
||||||
|
var file = ctx.uploadedFile("file");
|
||||||
|
if (file != null ) {
|
||||||
|
// Process the uploaded file
|
||||||
|
try(InputStream in = file.content()) {
|
||||||
|
var savetarget = Codes.Companion.getAudioFilePath().resolve(file.filename());
|
||||||
|
Files.copy(in, savetarget, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
logger.info("File uploaded: {}", file.filename());
|
||||||
|
ctx.status(HttpStatus.OK).result("File uploaded successfully: " + file.filename());
|
||||||
|
} catch (Exception e){
|
||||||
|
ctx.status(HttpStatus.INTERNAL_SERVER_ERROR).result("File upload failed: " + file.filename());
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
ctx.status(HttpStatus.BAD_REQUEST).result("No file uploaded");
|
||||||
|
}
|
||||||
|
});
|
||||||
ws("/ws", wshandler -> wshandler.onMessage(wsMessageContext -> {
|
ws("/ws", wshandler -> wshandler.onMessage(wsMessageContext -> {
|
||||||
// Handle incoming WebSocket messages
|
// Handle incoming WebSocket messages
|
||||||
String message = wsMessageContext.message();
|
String message = wsMessageContext.message();
|
||||||
|
//logger.info("Received message from setting.html/ws: {}", message);
|
||||||
try{
|
try{
|
||||||
var command = objectMapper.readValue(message, WsCommand.class);
|
var command = objectMapper.readValue(message, WsCommand.class);
|
||||||
logger.info("Received command from setting.html/ws : {}", command);
|
//logger.info("Received command from setting.html/ws : {}", command);
|
||||||
var reply = callback.apply("setting", command);
|
var reply = callback.apply("setting", command);
|
||||||
logger.info("Replying to setting.html/ws : {}", reply);
|
//logger.info("Replying to setting.html/ws : {}", reply);
|
||||||
wsMessageContext.send(reply);
|
wsMessageContext.send(reply);
|
||||||
} catch (Exception e){
|
} catch (Exception e){
|
||||||
logger.error("Error processing {} from setting.html/ws: {}", message, e.getMessage());
|
logger.error("Error processing {} from setting.html/ws: {}", message, e.getMessage());
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ package zello
|
|||||||
|
|
||||||
import com.fasterxml.jackson.module.kotlin.contains
|
import com.fasterxml.jackson.module.kotlin.contains
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.java_websocket.client.WebSocketClient
|
import org.java_websocket.client.WebSocketClient
|
||||||
import org.java_websocket.handshake.ServerHandshake
|
import org.java_websocket.handshake.ServerHandshake
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
@@ -20,23 +16,27 @@ import java.util.function.BiConsumer
|
|||||||
/**
|
/**
|
||||||
* ZelloClient is a WebSocket client for connecting to Zello services.
|
* ZelloClient is a WebSocket client for connecting to Zello services.
|
||||||
* [Source](https://github.com/zelloptt/zello-channel-api/blob/master/API.md)
|
* [Source](https://github.com/zelloptt/zello-channel-api/blob/master/API.md)
|
||||||
|
* @param address the WebSocket address of the Zello server, e.g. "wss://zello.io/ws"
|
||||||
|
* @param username the username for Zello authentication
|
||||||
|
* @param password the password for Zello authentication
|
||||||
|
* @param channel the default channel to join after connecting
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
class ZelloClient(val address : URI, val username: String, val password: String) {
|
class ZelloClient(val address : URI, val username: String, val password: String, val channel: String="GtcDev2025") {
|
||||||
private val streamJob = HashMap<Int, ZelloAudioJob>()
|
private val streamJob = HashMap<Int, ZelloAudioJob>()
|
||||||
private val imageJob = HashMap<Int, ZelloImageJob>()
|
private val imageJob = HashMap<Int, ZelloImageJob>()
|
||||||
private val commandJob = HashMap<Int, Any>()
|
private val commandJob = HashMap<Int, Any>()
|
||||||
companion object {
|
companion object {
|
||||||
fun fromConsumerZello(username : String, password: String) : ZelloClient {
|
fun fromConsumerZello(username : String, password: String, channel: String) : ZelloClient {
|
||||||
return ZelloClient(URI.create("wss://zello.io/ws"), username, password)
|
return ZelloClient(URI.create("wss://zello.io/ws"), username, password, channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fromZelloWork(username: String, password: String, networkName : String) : ZelloClient{
|
fun fromZelloWork(username: String, password: String, channel: String, networkName : String) : ZelloClient{
|
||||||
return ZelloClient(URI.create("wss://zellowork.io/ws/$networkName"), username, password)
|
return ZelloClient(URI.create("wss://zellowork.io/ws/$networkName"), username, password, channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fromZelloEnterpriseServer(username: String, password: String, serverDomain: String) : ZelloClient{
|
fun fromZelloEnterpriseServer(username: String, password: String, channel: String, serverDomain: String) : ZelloClient{
|
||||||
return ZelloClient(URI.create("wss://$serverDomain/ws/mesh"), username, password)
|
return ZelloClient(URI.create("wss://$serverDomain/ws/mesh"), username, password, channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,21 +45,29 @@ class ZelloClient(val address : URI, val username: String, val password: String)
|
|||||||
// this key is temporary, valid only 30 days from 2025-07-29
|
// this key is temporary, valid only 30 days from 2025-07-29
|
||||||
// if need to create, from https://developers.zello.com/keys
|
// if need to create, from https://developers.zello.com/keys
|
||||||
private val developerKey : String = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJXa002Y21ScllYSjBiMjV2T2pFLi1yYjJ2THFRbUhYV3dKY2I2azl2TDdUMEtzRWZMRjcxZm5jcktTZ0s2ZE0iLCJleHAiOjE3NTY0MzIyMTIsImF6cCI6ImRldiJ9.ANK7BIS6WVVWsQRjcZXyGWrV2RodCUQD4WXWaA6E4Dlyy8bBCMFdbiKN2D7B_x729HQULailnfRhbXF4Avfg14qONdc1XE_0iGiPUO1kfUSgdd11QylOzjxy6FTKSeZmHOh65JZq2dIWxobCcva-RPvbR8TA656upHh32xrWv9zlU0N707FTca04kze0Iq-q-uC5EL82yK10FEvOPDX88MYy71QRYi8Qh_KbSyMcYAhe2bTsiyjm51ZH9ntkRHd0HNiaijNZI6-qXkkp5Soqmzh-bTtbbgmbX4BT3Qpz_IP3epaX3jl_Aq5DHxXwCsJ9FThif9um5D0TWVGQteR0cQ"
|
private val developerKey : String = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJXa002Y21ScllYSjBiMjV2T2pFLi1yYjJ2THFRbUhYV3dKY2I2azl2TDdUMEtzRWZMRjcxZm5jcktTZ0s2ZE0iLCJleHAiOjE3NTY0MzIyMTIsImF6cCI6ImRldiJ9.ANK7BIS6WVVWsQRjcZXyGWrV2RodCUQD4WXWaA6E4Dlyy8bBCMFdbiKN2D7B_x729HQULailnfRhbXF4Avfg14qONdc1XE_0iGiPUO1kfUSgdd11QylOzjxy6FTKSeZmHOh65JZq2dIWxobCcva-RPvbR8TA656upHh32xrWv9zlU0N707FTca04kze0Iq-q-uC5EL82yK10FEvOPDX88MYy71QRYi8Qh_KbSyMcYAhe2bTsiyjm51ZH9ntkRHd0HNiaijNZI6-qXkkp5Soqmzh-bTtbbgmbX4BT3Qpz_IP3epaX3jl_Aq5DHxXwCsJ9FThif9um5D0TWVGQteR0cQ"
|
||||||
// default channel to join
|
|
||||||
private var channels = arrayOf("GtcDev2025")
|
|
||||||
// refresh token for the session
|
// refresh token for the session
|
||||||
// this is set after the first LogonReply
|
// this is set after the first LogonReply
|
||||||
private var refresh_token: String? = null
|
private var refresh_token: String? = null
|
||||||
|
|
||||||
private val logger : Logger = LoggerFactory.getLogger(ZelloClient::class.java)
|
private val logger : Logger = LoggerFactory.getLogger(ZelloClient::class.java)
|
||||||
private val mapper = jacksonObjectMapper()
|
private val mapper = jacksonObjectMapper()
|
||||||
|
|
||||||
|
var currentChannel: String? = null ; private set
|
||||||
|
var isOnline: Boolean = false ; private set
|
||||||
|
var isReceivingStreaming: Boolean = false
|
||||||
|
var receivingFrom: String? = null; private set
|
||||||
|
var bytesReceived: Long = 0; private set
|
||||||
|
|
||||||
fun Start(event: ZelloEvent){
|
fun Start(event: ZelloEvent){
|
||||||
client = object : WebSocketClient(address) {
|
client = object : WebSocketClient(address) {
|
||||||
override fun onOpen(handshakedata: ServerHandshake?) {
|
override fun onOpen(handshakedata: ServerHandshake?) {
|
||||||
//logger.info("Connected to $address")
|
//logger.info("Connected to $address")
|
||||||
|
isOnline = false
|
||||||
|
currentChannel = null
|
||||||
|
isReceivingStreaming = false
|
||||||
seqID = 0
|
seqID = 0
|
||||||
inc_seqID()
|
inc_seqID()
|
||||||
val lg = LogonCommand.create(seqID,channels, developerKey, username, password)
|
val lg = LogonCommand.create(seqID,arrayOf(channel), developerKey, username, password)
|
||||||
val value = mapper.writeValueAsString(lg)
|
val value = mapper.writeValueAsString(lg)
|
||||||
send(value)
|
send(value)
|
||||||
}
|
}
|
||||||
@@ -82,6 +90,7 @@ class ZelloClient(val address : URI, val username: String, val password: String)
|
|||||||
job.pushAudioData(data)
|
job.pushAudioData(data)
|
||||||
event.onStreamingData(job.from, job.For?:"", job.channel, data)
|
event.onStreamingData(job.from, job.For?:"", job.channel, data)
|
||||||
}
|
}
|
||||||
|
bytesReceived += data.size
|
||||||
|
|
||||||
}
|
}
|
||||||
0x02.toByte() ->{
|
0x02.toByte() ->{
|
||||||
@@ -119,7 +128,7 @@ class ZelloClient(val address : URI, val username: String, val password: String)
|
|||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun onMessage(message: String?) {
|
override fun onMessage(message: String?) {
|
||||||
logger.info("Message received: $message")
|
//logger.info("Message received: $message")
|
||||||
val jsnode = mapper.readTree(message)
|
val jsnode = mapper.readTree(message)
|
||||||
if (jsnode["seq"] != null) {
|
if (jsnode["seq"] != null) {
|
||||||
when(val seq = jsnode.get("seq").asInt()){
|
when(val seq = jsnode.get("seq").asInt()){
|
||||||
@@ -130,7 +139,7 @@ class ZelloClient(val address : URI, val username: String, val password: String)
|
|||||||
refresh_token = lgreply.refresh_token
|
refresh_token = lgreply.refresh_token
|
||||||
event.onConnected()
|
event.onConnected()
|
||||||
} else {
|
} else {
|
||||||
logger.error("Failed to logon: ${lgreply.error ?: "Unknown error"}")
|
event.onError("Failed to logon: ${lgreply.error ?: "Unknown error"}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else ->{
|
else ->{
|
||||||
@@ -163,6 +172,8 @@ class ZelloClient(val address : URI, val username: String, val password: String)
|
|||||||
"on_channel_status" -> {
|
"on_channel_status" -> {
|
||||||
val channelstatus = mapper.treeToValue(jsnode, Event_OnChannelStatus::class.java)
|
val channelstatus = mapper.treeToValue(jsnode, Event_OnChannelStatus::class.java)
|
||||||
event.onChannelStatus(channelstatus.channel, channelstatus.status, channelstatus.users_online, channelstatus.error, channelstatus.error_type)
|
event.onChannelStatus(channelstatus.channel, channelstatus.status, channelstatus.users_online, channelstatus.error, channelstatus.error_type)
|
||||||
|
currentChannel = channelstatus.channel
|
||||||
|
isOnline = channelstatus.status== "online"
|
||||||
}
|
}
|
||||||
"on_error" -> {
|
"on_error" -> {
|
||||||
val error = mapper.treeToValue(jsnode, Event_OnError::class.java)
|
val error = mapper.treeToValue(jsnode, Event_OnError::class.java)
|
||||||
@@ -173,6 +184,9 @@ class ZelloClient(val address : URI, val username: String, val password: String)
|
|||||||
logger.info("Stream started on channel ${streamstart.channel} from ${streamstart.from} for ${streamstart.For}")
|
logger.info("Stream started on channel ${streamstart.channel} from ${streamstart.from} for ${streamstart.For}")
|
||||||
streamJob[streamstart.stream_id] = ZelloAudioJob.fromEventOnStreamStart(streamstart)
|
streamJob[streamstart.stream_id] = ZelloAudioJob.fromEventOnStreamStart(streamstart)
|
||||||
event.onStartStreaming(streamstart.from, streamstart.For, streamstart.channel)
|
event.onStartStreaming(streamstart.from, streamstart.For, streamstart.channel)
|
||||||
|
isReceivingStreaming = true
|
||||||
|
receivingFrom = streamstart.from
|
||||||
|
bytesReceived = 0 // reset bytes received
|
||||||
}
|
}
|
||||||
"on_stream_stop" -> {
|
"on_stream_stop" -> {
|
||||||
val streamstop = mapper.treeToValue(jsnode, Event_OnStreamStop::class.java)
|
val streamstop = mapper.treeToValue(jsnode, Event_OnStreamStop::class.java)
|
||||||
@@ -184,6 +198,8 @@ class ZelloClient(val address : URI, val username: String, val password: String)
|
|||||||
event.onStopStreaming(job.from, job.For?:"", job.channel)
|
event.onStopStreaming(job.from, job.For?:"", job.channel)
|
||||||
event.onAudioData(job.streamID, job.from, job.For?:"", job.channel, job.getAudioData())
|
event.onAudioData(job.streamID, job.from, job.For?:"", job.channel, job.getAudioData())
|
||||||
}
|
}
|
||||||
|
isReceivingStreaming = false
|
||||||
|
receivingFrom = null
|
||||||
}
|
}
|
||||||
"on_image" ->{
|
"on_image" ->{
|
||||||
val image = mapper.treeToValue(jsnode, Event_OnImage::class.java)
|
val image = mapper.treeToValue(jsnode, Event_OnImage::class.java)
|
||||||
@@ -209,25 +225,10 @@ class ZelloClient(val address : URI, val username: String, val password: String)
|
|||||||
|
|
||||||
override fun onClose(code: Int, reason: String?, remote: Boolean) {
|
override fun onClose(code: Int, reason: String?, remote: Boolean) {
|
||||||
logger.info("Closed from ${if (remote) "Remote side" else "Local side"}, Code=$code, Reason=$reason")
|
logger.info("Closed from ${if (remote) "Remote side" else "Local side"}, Code=$code, Reason=$reason")
|
||||||
event.onDisconnected()
|
event.onDisconnected(reason?: "Unknown reason")
|
||||||
// try reconnecting after 10 seconds
|
isOnline = false
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
currentChannel = null
|
||||||
delay(10000)
|
|
||||||
connect()
|
|
||||||
}
|
|
||||||
|
|
||||||
//Revisi 06/08/2025 : Change to Coroutines
|
|
||||||
// val thread = Thread {
|
|
||||||
// try {
|
|
||||||
// Thread.sleep(10000)
|
|
||||||
// connect()
|
|
||||||
// } catch (e: InterruptedException) {
|
|
||||||
// logger.error("Reconnection interrupted: ${e.message}")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// thread.name= "ZelloClient-ReconnectThread"
|
|
||||||
// thread.isDaemon = true
|
|
||||||
// thread.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onError(ex: Exception?) {
|
override fun onError(ex: Exception?) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface ZelloEvent {
|
|||||||
fun onTextMessage(messageID: Int, from: String, For: String, channel: String, text: String, timestamp: Long)
|
fun onTextMessage(messageID: Int, from: String, For: String, channel: String, text: String, timestamp: Long)
|
||||||
fun onLocation(messageID: Int, from: String, For: String, channel: String, latitude: Double, longitude: Double, address:String, accuracy: Double, timestamp: Long )
|
fun onLocation(messageID: Int, from: String, For: String, channel: String, latitude: Double, longitude: Double, address:String, accuracy: Double, timestamp: Long )
|
||||||
fun onConnected()
|
fun onConnected()
|
||||||
fun onDisconnected()
|
fun onDisconnected(reason: String)
|
||||||
fun onError(errorMessage: String)
|
fun onError(errorMessage: String)
|
||||||
fun onStartStreaming(from: String, For: String, channel: String)
|
fun onStartStreaming(from: String, For: String, channel: String)
|
||||||
fun onStopStreaming(from: String, For: String, channel: String)
|
fun onStopStreaming(from: String, For: String, channel: String)
|
||||||
|
|||||||
Reference in New Issue
Block a user