diff --git a/html/webpage/assets/js/hardwarestatus.js b/html/webpage/assets/js/hardwarestatus.js new file mode 100644 index 0000000..2a86514 --- /dev/null +++ b/html/webpage/assets/js/hardwarestatus.js @@ -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); + } + } +}); \ No newline at end of file diff --git a/html/webpage/hardwarestatus.html b/html/webpage/hardwarestatus.html new file mode 100644 index 0000000..f3137ad --- /dev/null +++ b/html/webpage/hardwarestatus.html @@ -0,0 +1,260 @@ + + + + + + + EWS_POC + + + + + + +
+
+

Hardware Status

+
+
+
+

CPU (Average)

+
+
+

C0

+
+
+

C1

+
+
+

C2

+
+
+

C3

+
+
+

Temp

+
+
+
+
+

0.0

+
+
+

0.0

+
+
+

0.0

+
+
+

0.0

+
+
+

0.0

+
+
+

0

+
+
+
+
+

RAM Total

+
+
+

Free

+
+
+

Used

+
+
+

Free %

+
+
+
+
+

0.0

+
+
+

0.0

+
+
+

0.0

+
+
+

0.0

+
+
+
+
+

Disk Total

+
+
+

Free

+
+
+

Used

+
+
+

Free %

+
+
+
+
+

0.0

+
+
+

0.0

+
+
+

0.0

+
+
+

0.0

+
+
+
+
+

ETH 0 Status

+
+
+

IP

+
+
+

TX Bytes

+
+
+

RX Bytes

+
+
+

TX Speed

+
+
+

RX Speed

+
+
+
+
+

0.0

+
+
+

0.0

+
+
+

0.0

+
+
+

0.0

+
+
+

0.0

+
+
+

0

+
+
+
+
+

Modem Status

+
+
+

IP

+
+
+

TX Bytes

+
+
+

RX Bytes

+
+
+

TX Speed

+
+
+

RX Speed

+
+
+
+
+

0.0

+
+
+

0.0

+
+
+

0.0

+
+
+

0.0

+
+
+

0.0

+
+
+

0

+
+
+
+
+

Tunnel Status

+
+
+

IP

+
+
+

TX Bytes

+
+
+

RX Bytes

+
+
+

TX Speed

+
+
+

RX Speed

+
+
+
+
+

0.0

+
+
+

0.0

+
+
+

0.0

+
+
+

0.0

+
+
+

0.0

+
+
+

0

+
+
+
+ + + + + + \ No newline at end of file diff --git a/html/webpage/pocreceiver.html b/html/webpage/pocreceiver.html index 7d65085..df9f000 100644 --- a/html/webpage/pocreceiver.html +++ b/html/webpage/pocreceiver.html @@ -20,6 +20,7 @@ + Logout @@ -35,7 +36,7 @@

Zello Status

-

Paragraph

+

No Status

diff --git a/html/webpage/prerecordedbroadcast.html b/html/webpage/prerecordedbroadcast.html index 013b41a..1fec866 100644 --- a/html/webpage/prerecordedbroadcast.html +++ b/html/webpage/prerecordedbroadcast.html @@ -20,6 +20,7 @@
+ Logout diff --git a/html/webpage/setting.html b/html/webpage/setting.html index 9353efa..ca4c5b5 100644 --- a/html/webpage/setting.html +++ b/html/webpage/setting.html @@ -20,6 +20,7 @@ + Logout @@ -168,14 +169,9 @@

Content Upload

-
-
-
-
-
50%
-
-
-
+
+
+
diff --git a/src/Main.kt b/src/Main.kt index dc37b7d..96042d3 100644 --- a/src/Main.kt +++ b/src/Main.kt @@ -12,32 +12,110 @@ import zello.ZelloEvent import javafx.util.Pair import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import org.slf4j.LoggerFactory import sbc.DigitalOutput import sbc.NanopiDuo2 +import sbc.SbcInfo +import sbc.cpuinfo +import sbc.meminfo +import sbc.netdev import somecodes.Codes +import somecodes.Codes.Companion.isLinux import web.WsCommand import java.util.function.BiFunction //TIP To Run code, press or // click the icon in the gutter. fun main() { + // change when updating the application + val appversion = "0.1.1" val logger = LoggerFactory.getLogger("Main") val objectMapper = jacksonObjectMapper() - logger.info("Application started, version 0.0.8") + logger.info("Application started, version $appversion") val cfg = configFile() cfg.Load() + /** + * Coroutine scope for the application + */ + val appscope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + + /** + * various SBC related functions and information + */ + var sbc : SbcInfo? + + /** + * cpu information, one time read + */ + var cpuinfo: cpuinfo? + + /** + * ram information, will be updated periodically in appscope + */ + var raminfo: meminfo? + + /** + * cpu statistics, will be updated periodically in appscope + * key is cpu name + * value is cpu utilization percentage + * e.g. cpustat["cpu0"] = 12.5 + */ + val cpustat = HashMap() + + /** + * 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>() + + 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 - - runBlocking { + + appscope.launch { relay1 = DigitalOutput(NanopiDuo2.CLK.linuxGpio, "Relay1", true) relay2 = DigitalOutput(NanopiDuo2.MISO.linuxGpio, "Relay2", true) commandLED = DigitalOutput(NanopiDuo2.TX1.linuxGpio, "CommandLED", true) @@ -48,7 +126,6 @@ fun main() { streamingLED.setOFF() } - var audioID = 0 val preferedAudioDevice = "USB Audio" AudioUtility.LoadLibraries() @@ -147,12 +224,13 @@ fun main() { relay1?.setOFF() relay2?.setOFF() val e = this - CoroutineScope(Dispatchers.Default).launch { + appscope.launch { delay(10000) // Wait for 10 seconds before trying to reconnect z = CreateZelloFromConfig() z.Start(e) } + } override fun onError(errorMessage: String) { diff --git a/src/sbc/RaspberryPi4.kt b/src/sbc/RaspberryPi4.kt new file mode 100644 index 0000000..521999c --- /dev/null +++ b/src/sbc/RaspberryPi4.kt @@ -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"),; + + +} \ No newline at end of file diff --git a/src/sbc/SbcInfo.kt b/src/sbc/SbcInfo.kt new file mode 100644 index 0000000..1003868 --- /dev/null +++ b/src/sbc/SbcInfo.kt @@ -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>>) { + if (isLinux()) { + CoroutineScope(Dispatchers.Default).launch { + val map1 = HashMap() + val map2 = HashMap() + 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>() + 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? { + 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>>) { + 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>() + 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 + } + } +} \ No newline at end of file diff --git a/src/sbc/cpuinfo.kt b/src/sbc/cpuinfo.kt new file mode 100644 index 0000000..dd47ad2 --- /dev/null +++ b/src/sbc/cpuinfo.kt @@ -0,0 +1,3 @@ +package sbc + +data class cpuinfo(val core: Int, val hardware: String, val serialNumber: String) diff --git a/src/sbc/cpustat.kt b/src/sbc/cpustat.kt new file mode 100644 index 0000000..3db8748 --- /dev/null +++ b/src/sbc/cpustat.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/src/sbc/devstat.kt b/src/sbc/devstat.kt new file mode 100644 index 0000000..6382c99 --- /dev/null +++ b/src/sbc/devstat.kt @@ -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) + + diff --git a/src/sbc/meminfo.kt b/src/sbc/meminfo.kt new file mode 100644 index 0000000..f56b1eb --- /dev/null +++ b/src/sbc/meminfo.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/src/sbc/netdev.kt b/src/sbc/netdev.kt new file mode 100644 index 0000000..59e2e7c --- /dev/null +++ b/src/sbc/netdev.kt @@ -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" + } + } + } + + +} \ No newline at end of file diff --git a/src/somecodes/Codes.kt b/src/somecodes/Codes.kt index 75b3be8..cfc1949 100644 --- a/src/somecodes/Codes.kt +++ b/src/somecodes/Codes.kt @@ -16,9 +16,13 @@ class Codes { val gpioUnexportPath : Path = gpioPath.resolve("unexport") private val logger = LoggerFactory.getLogger("Codes") private val validAudioExtensions = setOf("wav", "mp3") - private const val KB_size = 1024 // 1 KB = 1024 bytes - private const val MB_size = 1024 * KB_size // 1 MB = 1024 KB - private const val GB_size = 1024 * MB_size // 1 GB = 1024 MB + 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()){ @@ -33,9 +37,9 @@ class Codes { fun SizeToString(size: Long) : String { return when { - size >= GB_size -> String.format("%.2f GB", size.toDouble() / GB_size) - size >= MB_size -> String.format("%.2f MB", size.toDouble() / MB_size) - size >= KB_size -> String.format("%.2f KB", size.toDouble() / KB_size) + 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" } }