diff --git a/README.md b/README.md index 937b9c722..e9fa02293 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,8 @@ PhotonVision was forked from [Chameleon Vision](https://github.com/Chameleon-Vis * [FasterXML](https://github.com/FasterXML) - Specifically [jackson](https://github.com/FasterXML/jackson) +* [OSHI](https://github.com/oshi/oshi) + ## License PhotonVision is licensed under the [GNU General Public License](https://www.gnu.org/licenses/gpl-3.0.html). diff --git a/photon-client/src/components/settings/MetricsCard.vue b/photon-client/src/components/settings/MetricsCard.vue index 952e7c4ad..5fb4dc2cc 100644 --- a/photon-client/src/components/settings/MetricsCard.vue +++ b/photon-client/src/components/settings/MetricsCard.vue @@ -1,7 +1,6 @@ @@ -128,10 +107,9 @@ onBeforeMount(() => { Metrics - - mdi-reload - Last Fetched: {{ metricsLastFetched }} - + + Last Update: {{ formattedDate.format(useSettingsStore().lastMetricsUpdate) }} + General @@ -215,6 +193,12 @@ onBeforeMount(() => { .metrics-table { width: 100%; text-align: center; + font-family: monospace !important; +} + +.metrics-update-time { + font-family: monospace !important; + font-size: 16px; } $stats-table-border: rgba(255, 255, 255, 0.5); diff --git a/photon-client/src/stores/settings/GeneralSettingsStore.ts b/photon-client/src/stores/settings/GeneralSettingsStore.ts index 28b0f7ad0..a8fbde94a 100644 --- a/photon-client/src/stores/settings/GeneralSettingsStore.ts +++ b/photon-client/src/stores/settings/GeneralSettingsStore.ts @@ -16,6 +16,7 @@ interface GeneralSettingsStore { network: NetworkSettings; lighting: LightingSettings; metrics: MetricData; + lastMetricsUpdate: Date; currentFieldLayout; } @@ -62,9 +63,12 @@ export const useSettingsStore = defineStore("settings", { gpuMem: undefined, gpuMemUtil: undefined, diskUtilPct: undefined, + diskUsableSpace: undefined, npuUsage: undefined, ipAddress: undefined, - uptime: undefined + uptime: undefined, + sentBitRate: undefined, + recvBitRate: undefined }, currentFieldLayout: { field: { @@ -72,7 +76,8 @@ export const useSettingsStore = defineStore("settings", { width: 8.2296 }, tags: [] - } + }, + lastMetricsUpdate: new Date() }), getters: { gpuAccelerationEnabled(): boolean { @@ -83,10 +88,8 @@ export const useSettingsStore = defineStore("settings", { } }, actions: { - requestMetricsUpdate() { - return axios.post("/utils/publishMetrics"); - }, updateMetricsFromWebsocket(data: Required) { + this.lastMetricsUpdate = new Date(); this.metrics = { cpuTemp: data.cpuTemp || undefined, cpuUtil: data.cpuUtil || undefined, @@ -96,9 +99,12 @@ export const useSettingsStore = defineStore("settings", { gpuMem: data.gpuMem || undefined, gpuMemUtil: data.gpuMemUtil || undefined, diskUtilPct: data.diskUtilPct || undefined, + diskUsableSpace: data.diskUsableSpace || undefined, npuUsage: data.npuUsage || undefined, ipAddress: data.ipAddress || undefined, - uptime: data.uptime || undefined + uptime: data.uptime || undefined, + sentBitRate: data.sentBitRate || undefined, + recvBitRate: data.recvBitRate || undefined }; }, updateGeneralSettingsFromWebsocket(data: WebsocketSettingsUpdate) { diff --git a/photon-client/src/types/SettingTypes.ts b/photon-client/src/types/SettingTypes.ts index bf949e91f..bab4547dc 100644 --- a/photon-client/src/types/SettingTypes.ts +++ b/photon-client/src/types/SettingTypes.ts @@ -34,9 +34,12 @@ export interface MetricData { gpuMem?: number; gpuMemUtil?: number; diskUtilPct?: number; + diskUsableSpace?: number; npuUsage?: number[]; ipAddress?: string; uptime?: number; + sentBitRate?: number; + recvBitRate?: number; } export enum NetworkConnectionType { diff --git a/photon-core/build.gradle b/photon-core/build.gradle index a120387b7..d65769c28 100644 --- a/photon-core/build.gradle +++ b/photon-core/build.gradle @@ -32,6 +32,7 @@ dependencies { implementation 'org.zeroturnaround:zt-zip:1.14' implementation "org.xerial:sqlite-jdbc:3.41.0.0" implementation 'com.diozero:diozero-core:1.4.1' + implementation 'com.github.oshi:oshi-core:6.9.1' // The JNI libraries use wpilibNatives, the java libraries use implementation if (jniPlatform == "linuxarm64") { diff --git a/photon-core/src/main/java/org/photonvision/common/configuration/HardwareConfig.java b/photon-core/src/main/java/org/photonvision/common/configuration/HardwareConfig.java index 0e5eda8ec..b6a72ca2c 100644 --- a/photon-core/src/main/java/org/photonvision/common/configuration/HardwareConfig.java +++ b/photon-core/src/main/java/org/photonvision/common/configuration/HardwareConfig.java @@ -41,17 +41,6 @@ public class HardwareConfig { public final String setPWMFrequencyCommand; public final String releaseGPIOCommand; - // Metrics - public final String cpuTempCommand; - public final String cpuMemoryCommand; - public final String cpuUtilCommand; - public final String cpuThrottleReasonCmd; - public final String cpuUptimeCommand; - public final String gpuMemoryCommand; - public final String ramUtilCommand; - public final String gpuMemUsageCommand; - public final String diskUsageCommand; - // Device stuff public final String restartHardwareCommand; public final double vendorFOV; // -1 for unmanaged @@ -71,15 +60,6 @@ public class HardwareConfig { String setPWMCommand, String setPWMFrequencyCommand, String releaseGPIOCommand, - String cpuTempCommand, - String cpuMemoryCommand, - String cpuUtilCommand, - String cpuThrottleReasonCmd, - String cpuUptimeCommand, - String gpuMemoryCommand, - String ramUtilCommand, - String gpuMemUsageCommand, - String diskUsageCommand, String restartHardwareCommand, double vendorFOV) { this.deviceName = deviceName; @@ -96,15 +76,6 @@ public class HardwareConfig { this.setPWMCommand = setPWMCommand; this.setPWMFrequencyCommand = setPWMFrequencyCommand; this.releaseGPIOCommand = releaseGPIOCommand; - this.cpuTempCommand = cpuTempCommand; - this.cpuMemoryCommand = cpuMemoryCommand; - this.cpuUtilCommand = cpuUtilCommand; - this.cpuThrottleReasonCmd = cpuThrottleReasonCmd; - this.cpuUptimeCommand = cpuUptimeCommand; - this.gpuMemoryCommand = gpuMemoryCommand; - this.ramUtilCommand = ramUtilCommand; - this.gpuMemUsageCommand = gpuMemUsageCommand; - this.diskUsageCommand = diskUsageCommand; this.restartHardwareCommand = restartHardwareCommand; this.vendorFOV = vendorFOV; } @@ -124,15 +95,6 @@ public class HardwareConfig { setPWMCommand = ""; setPWMFrequencyCommand = ""; releaseGPIOCommand = ""; - cpuTempCommand = ""; - cpuMemoryCommand = ""; - cpuUtilCommand = ""; - cpuThrottleReasonCmd = ""; - cpuUptimeCommand = ""; - gpuMemoryCommand = ""; - ramUtilCommand = ""; - gpuMemUsageCommand = ""; - diskUsageCommand = ""; restartHardwareCommand = ""; vendorFOV = -1; } @@ -144,21 +106,6 @@ public class HardwareConfig { return vendorFOV > 0; } - /** - * @return True if any info command has been configured to be non-empty, false otherwise - */ - public final boolean hasCommandsConfigured() { - return cpuTempCommand != "" - || cpuMemoryCommand != "" - || cpuUtilCommand != "" - || cpuThrottleReasonCmd != "" - || cpuUptimeCommand != "" - || gpuMemoryCommand != "" - || ramUtilCommand != "" - || gpuMemUsageCommand != "" - || diskUsageCommand != ""; - } - /** * @return True if any gpio command has been configured to be non-empty, false otherwise */ @@ -200,24 +147,6 @@ public class HardwareConfig { + setPWMFrequencyCommand + ", releaseGPIOCommand=" + releaseGPIOCommand - + ", cpuTempCommand=" - + cpuTempCommand - + ", cpuMemoryCommand=" - + cpuMemoryCommand - + ", cpuUtilCommand=" - + cpuUtilCommand - + ", cpuThrottleReasonCmd=" - + cpuThrottleReasonCmd - + ", cpuUptimeCommand=" - + cpuUptimeCommand - + ", gpuMemoryCommand=" - + gpuMemoryCommand - + ", ramUtilCommand=" - + ramUtilCommand - + ", gpuMemUsageCommand=" - + gpuMemUsageCommand - + ", diskUsageCommand=" - + diskUsageCommand + ", restartHardwareCommand=" + restartHardwareCommand + ", vendorFOV=" diff --git a/photon-core/src/main/java/org/photonvision/common/hardware/HardwareManager.java b/photon-core/src/main/java/org/photonvision/common/hardware/HardwareManager.java index a0fba6209..2da2173be 100644 --- a/photon-core/src/main/java/org/photonvision/common/hardware/HardwareManager.java +++ b/photon-core/src/main/java/org/photonvision/common/hardware/HardwareManager.java @@ -35,11 +35,9 @@ import org.photonvision.common.dataflow.networktables.NTDataChangeListener; import org.photonvision.common.dataflow.networktables.NetworkTablesManager; import org.photonvision.common.hardware.gpio.CustomAdapter; import org.photonvision.common.hardware.gpio.CustomDeviceFactory; -import org.photonvision.common.hardware.metrics.MetricsManager; import org.photonvision.common.logging.LogGroup; import org.photonvision.common.logging.Logger; import org.photonvision.common.util.ShellExec; -import org.photonvision.common.util.TimedTaskManager; public class HardwareManager { private static HardwareManager instance; @@ -50,8 +48,6 @@ public class HardwareManager { private final HardwareConfig hardwareConfig; private final HardwareSettings hardwareSettings; - private final MetricsManager metricsManager; - @SuppressWarnings({"FieldCanBeLocal", "unused"}) private final StatusLED statusLED; @@ -77,12 +73,6 @@ public class HardwareManager { this.hardwareConfig = hardwareConfig; this.hardwareSettings = hardwareSettings; - this.metricsManager = new MetricsManager(); - this.metricsManager.setConfig(hardwareConfig); - - TimedTaskManager.getInstance() - .addTask("Metrics Publisher", this.metricsManager::publishMetrics, 5000); - ledModeRequest = NetworkTablesManager.getInstance() .kRootTable @@ -259,8 +249,4 @@ public class HardwareManager { } statusLED.setStatus(status); } - - public void publishMetrics() { - metricsManager.publishMetrics(); - } } diff --git a/photon-core/src/main/java/org/photonvision/common/hardware/metrics/DeviceMetrics.java b/photon-core/src/main/java/org/photonvision/common/hardware/metrics/DeviceMetrics.java index 4c31a20fd..095936077 100644 --- a/photon-core/src/main/java/org/photonvision/common/hardware/metrics/DeviceMetrics.java +++ b/photon-core/src/main/java/org/photonvision/common/hardware/metrics/DeviceMetrics.java @@ -28,8 +28,11 @@ public record DeviceMetrics( double gpuMem, double gpuMemUtil, double diskUtilPct, + double diskUsableSpace, double[] npuUsage, String ipAddress, - double uptime) { + double uptime, + double sentBitRate, + double recvBitRate) { public static final DeviceMetricsProto proto = new DeviceMetricsProto(); } diff --git a/photon-core/src/main/java/org/photonvision/common/hardware/metrics/MetricsManager.java b/photon-core/src/main/java/org/photonvision/common/hardware/metrics/MetricsManager.java deleted file mode 100644 index c103c5d18..000000000 --- a/photon-core/src/main/java/org/photonvision/common/hardware/metrics/MetricsManager.java +++ /dev/null @@ -1,309 +0,0 @@ -/* - * Copyright (C) Photon Vision. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.photonvision.common.hardware.metrics; - -import edu.wpi.first.cscore.CameraServerJNI; -import edu.wpi.first.networktables.NetworkTable; -import edu.wpi.first.networktables.ProtobufPublisher; -import java.io.PrintWriter; -import java.io.StringWriter; -import org.photonvision.common.configuration.ConfigManager; -import org.photonvision.common.configuration.HardwareConfig; -import org.photonvision.common.dataflow.DataChangeService; -import org.photonvision.common.dataflow.events.OutgoingUIEvent; -import org.photonvision.common.dataflow.networktables.NetworkTablesManager; -import org.photonvision.common.hardware.Platform; -import org.photonvision.common.hardware.metrics.cmds.CmdBase; -import org.photonvision.common.hardware.metrics.cmds.FileCmds; -import org.photonvision.common.hardware.metrics.cmds.LinuxCmds; -import org.photonvision.common.hardware.metrics.cmds.PiCmds; -import org.photonvision.common.hardware.metrics.cmds.QCS6490Cmds; -import org.photonvision.common.hardware.metrics.cmds.RK3588Cmds; -import org.photonvision.common.logging.LogGroup; -import org.photonvision.common.logging.Logger; -import org.photonvision.common.networking.NetworkUtils; -import org.photonvision.common.util.ShellExec; - -public class MetricsManager { - final Logger logger = new Logger(MetricsManager.class, LogGroup.General); - - CmdBase cmds; - - ProtobufPublisher metricPublisher = - NetworkTablesManager.getInstance() - .kRootTable - .getSubTable("/metrics") - .getProtobufTopic(CameraServerJNI.getHostname(), DeviceMetrics.proto) - .publish(); - - private final ShellExec runCommand = new ShellExec(true, true); - - public void setConfig(HardwareConfig config) { - if (config.hasCommandsConfigured()) { - cmds = new FileCmds(); - } else if (Platform.isRaspberryPi()) { - cmds = new PiCmds(); // Pi's can use a hardcoded command set - } else if (Platform.isRK3588()) { - cmds = new RK3588Cmds(); // RK3588 chipset hardcoded command set - } else if (Platform.isQCS6490()) { - cmds = new QCS6490Cmds(); // QCS6490 chipset hardcoded command set - } else if (Platform.isLinux()) { - cmds = new LinuxCmds(); // Linux/Unix platforms assume a nominal command set - } else { - cmds = new CmdBase(); // default - base has no commands - } - - cmds.initCmds(config); - } - - public String safeExecute(String str) { - if (str.isEmpty()) return ""; - try { - return execute(str); - } catch (Exception e) { - return "****"; - } - } - - /** - * Get the CPU temperature in Celsius. - * - * @return The CPU temperature in Celsius, or -1.0 if the command fails or parsing fails. - */ - public double getCpuTemp() { - try { - return Double.parseDouble(safeExecute(cmds.cpuTemperatureCommand)); - } catch (NumberFormatException e) { - return -1.0; - } - } - - /** - * Get the CPU utilization as a percentage. - * - * @return The CPU utilization as a percentage, or -1.0 if the command fails or parsing fails. - */ - public double getCpuUtilization() { - try { - return Double.parseDouble(safeExecute(cmds.cpuUtilizationCommand)); - } catch (NumberFormatException e) { - return -1.0; - } - } - - /** - * Get the reason for CPU throttling, if applicable. - * - * @return A string describing the CPU throttle reason, or an empty string if the command fails. - */ - public String getThrottleReason() { - return safeExecute(cmds.cpuThrottleReasonCmd); - } - - private double ramMemSave = -2.0; - - /** - * Get the total RAM memory in MB. This only runs once, as it won't change over time. - * - * @return The total RAM memory in MB, or -1.0 if the command fails or parsing fails. - */ - public double getRamMem() { - if (ramMemSave == -2.0) { - try { - ramMemSave = Double.parseDouble(safeExecute(cmds.ramMemCommand)); - } catch (NumberFormatException e) { - ramMemSave = -1.0; - } - } - return ramMemSave; - } - - /** - * Get the RAM utilization in MBs. - * - * @return The RAM utilization in MBs, or -1.0 if the command fails or parsing fails. - */ - public double getRamUtil() { - try { - return Double.parseDouble(safeExecute(cmds.ramUtilCommand)); - } catch (NumberFormatException e) { - return -1.0; - } - } - - private double gpuMemSave = -2.0; - - /** - * Get the total GPU memory in MB. This only runs once, as it won't change over time. - * - * @return The total GPU memory in MB, or -1.0 if the command fails or parsing fails. - */ - public double getGpuMem() { - if (gpuMemSave == -2.0) { - try { - gpuMemSave = Double.parseDouble(safeExecute(cmds.gpuMemCommand)); - } catch (NumberFormatException e) { - gpuMemSave = -1.0; - } - } - return gpuMemSave; - } - - /** - * Get the GPU memory utilization as MBs. - * - * @return The GPU memory utilization in MBs, or -1.0 if the command fails or parsing fails. - */ - public double getGpuMemUtil() { - try { - return Double.parseDouble(safeExecute(cmds.gpuMemUtilCommand)); - } catch (NumberFormatException e) { - return -1.0; - } - } - - /** - * Get the percentage of disk space used. - * - * @return The percentage of disk space used, or -1.0 if the command fails or parsing fails. - */ - public double getUsedDiskPct() { - try { - return Double.parseDouble(safeExecute(cmds.diskUsageCommand)); - } catch (NumberFormatException e) { - return -1.0; - } - } - - // This is here so we don't spam logs if it fails - boolean npuParseWarning = false; - - /** - * Get the NPU usage as an array of doubles. - * - * @return An array of doubles representing NPU usage, or null if parsing fails. - */ - public double[] getNpuUsage() { - if (cmds.npuUsageCommand.isBlank()) { - return new double[0]; - } - String[] usages = safeExecute(cmds.npuUsageCommand).split(","); - double[] usageDoubles = new double[usages.length]; - for (int i = 0; i < usages.length; i++) { - try { - usageDoubles[i] = Double.parseDouble(usages[i]); - npuParseWarning = false; // Reset warning if parsing succeeds - } catch (NumberFormatException e) { - if (!npuParseWarning) { - logger.error("Failed to parse NPU usage value: " + usages[i], e); - npuParseWarning = true; - } - usageDoubles = new double[0]; // Default to empty array if parsing fails - break; - } - } - return usageDoubles; - } - - /** - * Get the IP address of the device. - * - * @return The IP address as a string, or an empty string if the command fails. - */ - public String getIpAddress() { - String dev = ConfigManager.getInstance().getConfig().getNetworkConfig().networkManagerIface; - String addr = NetworkUtils.getIPAddresses(dev); - return addr; - } - - /** - * Get the uptime of the device in seconds. - * - * @return The uptime in seconds, or -1.0 if the command fails or parsing fails. - */ - public double getUptime() { - try { - return Double.parseDouble(safeExecute(cmds.uptimeCommand)); - } catch (NumberFormatException e) { - return -1.0; - } - } - - public void publishMetrics() { - // Check that the hostname hasn't changed - if (!CameraServerJNI.getHostname() - .equals(NetworkTable.basenameKey(metricPublisher.getTopic().getName()))) { - logger.warn("Metrics publisher name does not match hostname! Reinitializing publisher..."); - metricPublisher.close(); - metricPublisher = - NetworkTablesManager.getInstance() - .kRootTable - .getSubTable("/metrics") - .getProtobufTopic(CameraServerJNI.getHostname(), DeviceMetrics.proto) - .publish(); - } - - var metrics = - new DeviceMetrics( - this.getCpuTemp(), - this.getCpuUtilization(), - this.getThrottleReason(), - this.getRamMem(), - this.getRamUtil(), - this.getGpuMem(), - this.getGpuMemUtil(), - this.getUsedDiskPct(), - this.getNpuUsage(), - this.getIpAddress(), - this.getUptime()); - - metricPublisher.set(metrics); - - DataChangeService.getInstance().publishEvent(OutgoingUIEvent.wrappedOf("metrics", metrics)); - } - - public synchronized String execute(String command) { - try { - runCommand.executeBashCommand(command, true, false); - return runCommand.getOutput(); - } catch (Exception e) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - - logger.error( - "Command: \"" - + command - + "\" returned an error!" - + "\nOutput Received: " - + runCommand.getOutput() - + "\nStandard Error: " - + runCommand.getError() - + "\nCommand completed: " - + runCommand.isOutputCompleted() - + "\nError completed: " - + runCommand.isErrorCompleted() - + "\nExit code: " - + runCommand.getExitCode() - + "\n Exception: " - + e - + sw); - return ""; - } - } -} diff --git a/photon-core/src/main/java/org/photonvision/common/hardware/metrics/SystemMonitor.java b/photon-core/src/main/java/org/photonvision/common/hardware/metrics/SystemMonitor.java new file mode 100644 index 000000000..4501e0fea --- /dev/null +++ b/photon-core/src/main/java/org/photonvision/common/hardware/metrics/SystemMonitor.java @@ -0,0 +1,563 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonvision.common.hardware.metrics; + +import edu.wpi.first.cscore.CameraServerJNI; +import edu.wpi.first.networktables.NetworkTable; +import edu.wpi.first.networktables.ProtobufPublisher; +import java.io.IOException; +import java.nio.file.FileStore; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.photonvision.common.configuration.ConfigManager; +import org.photonvision.common.dataflow.DataChangeService; +import org.photonvision.common.dataflow.events.OutgoingUIEvent; +import org.photonvision.common.dataflow.networktables.NetworkTablesManager; +import org.photonvision.common.hardware.Platform; +import org.photonvision.common.logging.LogGroup; +import org.photonvision.common.logging.Logger; +import org.photonvision.common.networking.NetworkUtils; +import org.photonvision.common.util.TimedTaskManager; +import org.photonvision.common.util.file.ProgramDirectoryUtilities; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; +import oshi.hardware.CentralProcessor.PhysicalProcessor; +import oshi.hardware.GlobalMemory; +import oshi.hardware.GraphicsCard; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.hardware.NetworkIF; +import oshi.software.os.OperatingSystem; +import oshi.util.FormatUtil; +import oshi.util.GlobalConfig; + +public class SystemMonitor { + protected static Logger logger = new Logger(SystemMonitor.class, LogGroup.General); + + private static SystemMonitor instance; + + private record NetworkTraffic(double sentBitRate, double recvBitRate) {} + + ProtobufPublisher metricPublisher = + NetworkTablesManager.getInstance() + .kRootTable + .getSubTable("/metrics") + .getProtobufTopic(CameraServerJNI.getHostname(), DeviceMetrics.proto) + .publish(); + + private SystemInfo si; + private CentralProcessor cpu; + private OperatingSystem os; + private GlobalMemory mem; + private HardwareAbstractionLayer hal; + private FileStore fs; + + private double totalMemory = -1.0; + + private double lastCpuLoad = 0; + private long lastCpuUpdate = 0; + private long[] oldTicks; + + private NetworkIF monitoredIFace = null; + private long lastTrafficUpdate = 0; + private long lastBytesSent = 0; + private long lastBytesRecv = 0; + private NetworkTraffic lastResult = new NetworkTraffic(0, 0); + + // Set this to true to enable logging the contents of the DeviceMetrics class that is sent to NT + // and the UI. + public boolean writeMetricsToLog = false; + + private final String taskName = "SystemMonitorPublisher"; + private final double minimumDeltaTime = 0.250; // seconds + private final long mebi = (1024 * 1024); + + /** + * Returns the singleton instance of SystemMonitor. Creates the instance, thereby initializing it, + * on the first call. + * + * @return instance of SystemMonitor. + */ + public static SystemMonitor getInstance() { + if (instance == null) { + if (Platform.isRaspberryPi()) { + instance = new SystemMonitorRaspberryPi(); + } else if (Platform.isRK3588()) { + instance = new SystemMonitorRK3588(); + } else if (Platform.isQCS6490()) { + instance = new SystemMonitorQCS6490(); + } else { + instance = new SystemMonitor(); + } + } + return instance; + } + + protected SystemMonitor() { + logger.info("Starting SystemMonitor"); + GlobalConfig.set(GlobalConfig.OSHI_OS_WINDOWS_LOADAVERAGE, true); + GlobalConfig.set("oshi.os.linux.sensors.cpuTemperature.types", getThermalZoneTypes()); + + si = new SystemInfo(); + hal = si.getHardware(); + os = si.getOperatingSystem(); + cpu = hal.getProcessor(); + mem = hal.getMemory(); + + try { + // get the filesystem for the directory photonvision is running in + fs = Files.getFileStore(Path.of(ProgramDirectoryUtilities.getProgramDirectory())); + } catch (IOException e) { + logger.error("Couldn't get FileStore for " + Path.of("")); + fs = null; + } + + // initialize CPU monitoring + oldTicks = cpu.getSystemCpuLoadTicks(); + + // initialize network traffic monitoring + selectNetworkIfByName( + ConfigManager.getInstance().getConfig().getNetworkConfig().networkManagerIface); + } + + /** + * Returns a comma-separated list of addtional thermal zone types that should be checked to get + * the CPU temperature on Unix systems. The temperature will be reported for the first temperature + * zone with a type that mateches an item of this list. If the CPU temperature isn't being + * reported correctly for a coprocessor, override this method to return a string with type + * associated with the thermal zone for that comprocessor. + * + * @return String containing a comma-separated list of thermal zone types for reading CPU + * temperature. + */ + protected String getThermalZoneTypes() { + // Find the thermal zone type by logging on to the coprocessor and running: + // `cat /sys/class/thermal/thermal_zone*/type` + // This command will show the types for all thermal zones. + // + return GlobalConfig.get("oshi.os.linux.sensors.cpuTemperature.types"); + } + + /** + * Starts the periodic system monitor that publishes performance metrics. The metrics are + * published every millisUpdateInerval seconds after a millisStartDelay startup delay. Calling + * this method when the monitor is running will stop it and restart it with the new delay and + * update interval. + * + * @param millisStartDelay the delay before the metrics are first published. + * @param millisUpdateInterval the time between updates in units of milliseconds. + */ + public void startMonitor(long millisStartDelay, long millisUpdateInterval) { + if (TimedTaskManager.getInstance().taskActive(taskName)) { + logger.debug("Stopping running SystemMonitor!"); + TimedTaskManager.getInstance().cancelTask(taskName); + } + logger.debug("Starting SystemMonitor with " + millisUpdateInterval + " ms update interval."); + TimedTaskManager.getInstance() + .addTask(taskName, this::publishMetrics, millisStartDelay, millisUpdateInterval); + } + + private void publishMetrics() { + // Check that the hostname hasn't changed + if (!CameraServerJNI.getHostname() + .equals(NetworkTable.basenameKey(metricPublisher.getTopic().getName()))) { + logger.warn("Metrics publisher name does not match hostname! Reinitializing publisher..."); + metricPublisher.close(); + metricPublisher = + NetworkTablesManager.getInstance() + .kRootTable + .getSubTable("/metrics") + .getProtobufTopic(CameraServerJNI.getHostname(), DeviceMetrics.proto) + .publish(); + } + + var nt = this.getNetworkTraffic(); + var metrics = + new DeviceMetrics( + this.getCpuTemperature(), + this.getCpuUsage(), + this.getCpuThrottleReason(), + this.getTotalMemory(), + this.getUsedMemory(), + this.getGpuMem(), + this.getGpuMemUtil(), + this.getUsedDiskPct(), + this.getUsableDiskSpace(), + this.getNpuUsage(), + this.getIpAddress(), + this.getUptime(), + nt.sentBitRate, + nt.recvBitRate); + + metricPublisher.set(metrics); + + if (writeMetricsToLog) { + logMetrics(metrics); + } + + DataChangeService.getInstance().publishEvent(OutgoingUIEvent.wrappedOf("metrics", metrics)); + } + + private void logMetrics(DeviceMetrics metrics) { + StringBuilder sb = new StringBuilder(); + sb.append("System Metrics Update: "); + sb.append(String.format("System Uptime: %.0f, ", metrics.uptime())); + sb.append(String.format("CPU Usage: %.2f%%, ", metrics.cpuUtil())); + sb.append(String.format("CPU Temperature: %.2f °C, ", metrics.cpuTemp())); + sb.append(String.format("NPU Usage: %s, ", Arrays.toString(metrics.npuUsage()))); + sb.append(String.format("Used Disk: %.2f%%, ", metrics.diskUtilPct())); + sb.append(String.format("Usable Disk Space: %.0f MiB, ", metrics.diskUsableSpace() / mebi)); + sb.append(String.format("Memory: %.0f / %.0f MiB, ", metrics.ramUtil(), metrics.ramMem())); + sb.append( + String.format("GPU Memory: %.0f / %.0f MiB, ", metrics.gpuMemUtil(), metrics.gpuMem())); + sb.append( + String.format("CPU Throttle: %s, ", metrics.cpuThr().isBlank() ? "N/A" : metrics.cpuThr())); + sb.append( + String.format( + "Data sent: %.0f Kbps, Data recieved: %.0f Kbps", + metrics.sentBitRate() / 1000, metrics.recvBitRate() / 1000)); + logger.debug(sb.toString()); + } + + private void resetNetworkTraffic() { + lastBytesSent = monitoredIFace.getBytesSent(); + lastBytesRecv = monitoredIFace.getBytesRecv(); + lastTrafficUpdate = System.currentTimeMillis(); + } + + private NetworkIF selectNetworkIfByName(String name) { + if (name.isBlank() || monitoredIFace != null && monitoredIFace.getName().equals(name)) { + return monitoredIFace; + } + for (var iFace : hal.getNetworkIFs()) { + if (iFace.getName().equals(name)) { + logger.debug("Monitoring network traffic on '" + name + "'"); + monitoredIFace = iFace; + resetNetworkTraffic(); + return iFace; + } + } + logger.warn("Can't monitor network interface '" + name + "'"); + return null; + } + + /** Writes available information about the hardware to the log. */ + public void logSystemInformation() { + var sb = new StringBuilder(); + sb.append("*** System Information ***\n"); + sb.append("Operating System: " + os.toString() + "\n"); + sb.append(" System Uptime: " + FormatUtil.formatElapsedSecs(getUptime()) + "\n"); + sb.append(" Elevated Privileges: " + os.isElevated() + "\n"); + + var computerSystem = hal.getComputerSystem(); + sb.append("System: " + computerSystem.toString() + "\n"); + sb.append(" Manufacturer: " + computerSystem.getManufacturer() + "\n"); + sb.append(" Firmware: " + computerSystem.getFirmware() + "\n"); + sb.append(" Baseboard: " + computerSystem.getBaseboard() + "\n"); + sb.append(" Model: " + computerSystem.getModel() + "\n"); + sb.append(" Serial Number: " + computerSystem.getSerialNumber() + "\n"); + + sb.append("CPU Info: " + cpu.toString() + "\n"); + sb.append(" Max Frequency: " + FormatUtil.formatHertz(cpu.getMaxFreq()) + "\n"); + sb.append( + " Current Frequency: " + + Arrays.stream(cpu.getCurrentFreq()) + .mapToObj(FormatUtil::formatHertz) + .collect(Collectors.joining(", ")) + + "\n"); + for (PhysicalProcessor core : cpu.getPhysicalProcessors()) { + sb.append( + " Core " + core.getPhysicalProcessorNumber() + " (" + core.getEfficiency() + ")\n"); + } + var myProc = os.getCurrentProcess(); + sb.append("Current Process: " + myProc.getName() + ", PID: " + myProc.getProcessID() + "\n"); + // sb.append(" Command Line: " + myProc.getCommandLine()); + sb.append(" Kernel Time: " + myProc.getKernelTime() + "\n"); + sb.append(" User Time: " + myProc.getUserTime() + "\n"); + sb.append(" Cumulative Load: " + myProc.getProcessCpuLoadCumulative() + "\n"); + sb.append(" Up Time: " + myProc.getUpTime() + "\n"); + sb.append(" Priority: " + myProc.getPriority() + "\n"); + sb.append(" User: " + myProc.getUser() + "\n"); + sb.append(" Threads: " + myProc.getThreadCount() + "\n"); + + sb.append("Network Interfaces\n"); + for (NetworkIF iFace : hal.getNetworkIFs()) { + sb.append(" Interface: " + iFace.toString() + "\n"); + } + + sb.append("Graphics Cards\n"); + for (GraphicsCard gc : hal.getGraphicsCards()) { + sb.append(" Card: " + gc.toString() + "\n"); + } + logger.info(sb.toString()); + } + + /** + * Returns the total space available (in bytes) for the filesystem where PhotonVision is running + * (typically "/"). This doesn't report on other mounted filesystems, such as USB sticks. + * + * @return the number of bytes total, or -1 if the command fails. + */ + public long getTotalDiskSpace() { + if (fs != null) { + try { + return fs.getTotalSpace(); + } catch (IOException e) { + logger.error("Couldn't retrieve total disk space", e); + } + } + return -1; + } + + /** + * Returns the free space available (in bytes) for the filesystem where PhotonVision is running + * (typically "/"). This doesn't report on other mounted filesystems, such as USB sticks. + * + * @return the number of bytes available, or -1 if the command fails. + */ + public long getUsableDiskSpace() { + if (fs != null) { + try { + return fs.getUsableSpace(); + } catch (IOException e) { + logger.error("Couldn't retrieve usable disk space", e); + } + } + return -1; + } + + /** + * Returns the percentage of disk space used. + * + * @return The percentage of disk space used, or -1.0 if the command fails. + */ + public double getUsedDiskPct() { + double usedPct; + if (fs == null) return -1.0; + try { + double total = fs.getTotalSpace(); + // note: df matches better with fs.getUnallocatedSpace(), but this is more conservative + usedPct = 100.0 * (1.0 - fs.getUsableSpace() / total); + } catch (IOException e) { + logger.error("Couldn't retrieve used disk space", e); + usedPct = -1.0; + } + return usedPct; + } + + /** + * Returns the temperature of the CPU. + * + * @return The temperature of the CPU in °C or -1.0 if it cannot be retrieved. + */ + public double getCpuTemperature() { + double temperature = hal.getSensors().getCpuTemperature(); + // OSHI returns 0 or NaN if the temperature isn't available. + if (temperature == 0.0 || Double.isNaN(temperature)) { + temperature = -1.0; + } + return temperature; + } + + /** + * Returns the total RAM. + * + * @return total RAM in MiB. + */ + public double getTotalMemory() { + if (totalMemory < 0) { + totalMemory = mem.getTotal() / mebi; + } + return totalMemory; + } + + /** + * Returns the amount of memory in use. + * + * @return the used RAM in MiB. + */ + public double getUsedMemory() { + return (mem.getTotal() - mem.getAvailable()) / mebi; + } + + /** + * Returns the time since system boot in seconds. + * + * @return the uptime in seconds. + */ + public long getUptime() { + return os.getSystemUptime(); + } + + /** + * Returns the average load on the CPU from 0 to 100% since last called by using the tick + * counters. + * + * @return load on the cpu in %. + */ + public synchronized double getCpuUsage() { + long now = System.currentTimeMillis(); + double dTime = (now - lastCpuUpdate) / 1000.0; + if (dTime > minimumDeltaTime) { + var newTicks = cpu.getSystemCpuLoadTicks(); + lastCpuLoad = 100 * cpu.getSystemCpuLoadBetweenTicks(oldTicks, newTicks); + oldTicks = newTicks; + lastCpuUpdate = now; + } + return lastCpuLoad; + } + + /** + * Returns the npu usage, if available. Platforms with NPUs will need to override this method to + * return a useful value. + * + * @return the NPU usage or an empty array if not available. + */ + public double[] getNpuUsage() { + return new double[0]; + } + + /** + * Returns a description of the CPU throttle state, if available. Platforms that provide this + * information will need to override this method to return a useful value. + * + * @return the CPU throttle state, or an empty String if not available. + */ + public String getCpuThrottleReason() { + return ""; + } + + /** + * Returns the total GPU memory in MiB. + * + * @return The total GPU memory in MiB, or -1.0 if not avaialable on this platform. + */ + public double getGpuMem() { + return -1.0; + } + + /** + * Returns the GPU memory utilization as MiBs. + * + * @return The GPU memory utilization in MiBs, or -1.0 if not available on this platform. + */ + public double getGpuMemUtil() { + return -1.0; + } + + /** + * Returns the IP address of the device. + * + * @return The IP address as a string, or an empty string if the command fails. + */ + public String getIpAddress() { + String dev = ConfigManager.getInstance().getConfig().getNetworkConfig().networkManagerIface; + return NetworkUtils.getIPAddresses(dev); + } + + /** + * Returns a NetworkTraffic instance containing the average sent and recieved network traffic + * since the last time this was called. + * + * @return NetworkTraffic instance with data in bits/second. The traffic values will be -1 if the + * data isn't available. + */ + private synchronized NetworkTraffic getNetworkTraffic() { + String activeIFaceName = + ConfigManager.getInstance().getConfig().getNetworkConfig().networkManagerIface; + var iFace = selectNetworkIfByName(activeIFaceName); + if (iFace == null) { + return new NetworkTraffic(-1, -1); + } + long now = System.currentTimeMillis(); + double dTime = (now - lastTrafficUpdate) / 1000.0; + if (dTime > minimumDeltaTime) { + // only update if it's been long enough since the last update + // otherwise, return the last value + iFace.updateAttributes(); + long bytesSent = iFace.getBytesSent(); + long bytesRecv = iFace.getBytesRecv(); + double sentBitRate = 8 * (bytesSent - lastBytesSent) / dTime; + double recvBitRate = 8 * (bytesRecv - lastBytesRecv) / dTime; + lastBytesSent = bytesSent; + lastBytesRecv = bytesRecv; + lastResult = new NetworkTraffic(sentBitRate, recvBitRate); + lastTrafficUpdate = now; + } + return lastResult; + } + + /** + * Benchmarks SystemMonitor by timing the calls to retrieve metrics and writes the results to the + * log. + */ + private void testSM() { + StringBuilder sb = new StringBuilder(); + double total = 0; + + sb.append("SystemMetrics Test:\n"); + total += timeIt(sb, () -> String.format("System Uptime: %d", getUptime())); + total += timeIt(sb, () -> String.format("CPU Usage: %.2f%%", getCpuUsage())); + total += timeIt(sb, () -> String.format("CPU Temperature: %.2f °C", getCpuTemperature())); + total += timeIt(sb, () -> String.format("NPU Usage: %s", Arrays.toString(getNpuUsage()))); + total += timeIt(sb, () -> String.format("Used Disk: %.2f%%", getUsedDiskPct())); + total += + timeIt( + sb, () -> String.format("Usable Disk Space: %.0f MiB, ", getUsableDiskSpace() / mebi)); + total += + timeIt( + sb, () -> String.format("Memory: %.0f / %.0f MiB", getUsedMemory(), getTotalMemory())); + total += + timeIt( + sb, () -> String.format("GPU Memory: %.0f / %.0f MiB", getGpuMemUtil(), getGpuMem())); + total += timeIt(sb, () -> String.format("CPU Throttle: %s", getCpuThrottleReason())); + + total += + timeIt( + sb, + () -> { + var nt = getNetworkTraffic(); + return String.format( + "Data sent: %.0f Kbps, Data recieved: %.0f Kbps", + nt.sentBitRate() / 1000, nt.recvBitRate() / 1000); + }); + + sb.append(String.format("==========\n%7.3f ms\n", total)); + + logger.info(sb.toString()); + } + + /** + * Updates a StringBuilder with the result of calling `source` prepended by the time required to + * run `source`, and returns the time (in ms) that a String Supplier takes. This can be used to + * compare different ways of gathering the same metric. + * + * @param sb A StringBuilder used to collect the output from the supplier. + * @param source A supplier that takes no arguments and returns a String. + * @return The time (in ms) required to produce the output. + */ + private double timeIt(StringBuilder sb, Supplier source) { + long start = System.nanoTime(); + String resp = source.get(); + var delta = (System.nanoTime() - start) / 1000000.0; + sb.append(String.format(" %7.3f ms >> %s\n", delta, resp)); + return delta; + } +} diff --git a/photon-core/src/main/java/org/photonvision/common/hardware/metrics/cmds/FileCmds.java b/photon-core/src/main/java/org/photonvision/common/hardware/metrics/SystemMonitorQCS6490.java similarity index 51% rename from photon-core/src/main/java/org/photonvision/common/hardware/metrics/cmds/FileCmds.java rename to photon-core/src/main/java/org/photonvision/common/hardware/metrics/SystemMonitorQCS6490.java index 5210453b7..658a558d1 100644 --- a/photon-core/src/main/java/org/photonvision/common/hardware/metrics/cmds/FileCmds.java +++ b/photon-core/src/main/java/org/photonvision/common/hardware/metrics/SystemMonitorQCS6490.java @@ -15,25 +15,11 @@ * along with this program. If not, see . */ -package org.photonvision.common.hardware.metrics.cmds; +package org.photonvision.common.hardware.metrics; -import org.photonvision.common.configuration.HardwareConfig; - -public class FileCmds extends CmdBase { +public class SystemMonitorQCS6490 extends SystemMonitor { @Override - public void initCmds(HardwareConfig config) { - cpuTemperatureCommand = config.cpuTempCommand; - cpuUtilizationCommand = config.cpuUtilCommand; - cpuThrottleReasonCmd = config.cpuThrottleReasonCmd; - - ramMemCommand = config.cpuMemoryCommand; - ramUtilCommand = config.ramUtilCommand; - - gpuMemCommand = config.gpuMemoryCommand; - gpuMemUtilCommand = config.gpuMemUsageCommand; - - diskUsageCommand = config.diskUsageCommand; - - uptimeCommand = config.cpuUptimeCommand; + protected String getThermalZoneTypes() { + return "cpu0-thermal"; } } diff --git a/photon-core/src/main/java/org/photonvision/common/hardware/metrics/cmds/RK3588Cmds.java b/photon-core/src/main/java/org/photonvision/common/hardware/metrics/SystemMonitorRK3588.java similarity index 67% rename from photon-core/src/main/java/org/photonvision/common/hardware/metrics/cmds/RK3588Cmds.java rename to photon-core/src/main/java/org/photonvision/common/hardware/metrics/SystemMonitorRK3588.java index b8be38ac6..ee58fa68c 100644 --- a/photon-core/src/main/java/org/photonvision/common/hardware/metrics/cmds/RK3588Cmds.java +++ b/photon-core/src/main/java/org/photonvision/common/hardware/metrics/SystemMonitorRK3588.java @@ -15,15 +15,20 @@ * along with this program. If not, see . */ -package org.photonvision.common.hardware.metrics.cmds; +package org.photonvision.common.hardware.metrics; -import org.photonvision.common.configuration.HardwareConfig; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.regex.Matcher; +import java.util.regex.Pattern; -public class RK3588Cmds extends LinuxCmds { - /** Applies pi-specific commands, ignoring any input configuration */ - public void initCmds(HardwareConfig config) { - super.initCmds(config); +public class SystemMonitorRK3588 extends SystemMonitor { + final String regex = "Core\\d:\\s*(\\d+)%"; + final Pattern pattern = Pattern.compile(regex); + @Override + protected String getThermalZoneTypes() { // CPU Temperature /* The RK3588 chip has 7 thermal zones that can be accessed via: * /sys/class/thermal/thermal_zoneX/temp @@ -42,10 +47,19 @@ public class RK3588Cmds extends LinuxCmds { * - http://forum.armsom.org/t/topic/51/3 * - https://lore.kernel.org/lkml/7276280.TLKafQO6qx@archbook/ */ - cpuTemperatureCommand = - "cat /sys/class/thermal/thermal_zone1/temp | awk '{printf \"%.1f\", $1/1000}'"; + return "bigcore0-thermal"; + } - npuUsageCommand = - "cat /sys/kernel/debug/rknpu/load | grep -o '[0-9]\\+%' | sed 's/%//g' | paste -sd ','"; + @Override + public double[] getNpuUsage() { + try { + var contents = Files.readString(Path.of("/sys/kernel/debug/rknpu/load")); + Matcher matcher = pattern.matcher(contents); + double[] results = + matcher.results().map(mr -> mr.group(1)).mapToDouble(Double::parseDouble).toArray(); + return results; + } catch (IOException e) { + return new double[0]; + } } } diff --git a/photon-core/src/main/java/org/photonvision/common/hardware/metrics/SystemMonitorRaspberryPi.java b/photon-core/src/main/java/org/photonvision/common/hardware/metrics/SystemMonitorRaspberryPi.java new file mode 100644 index 000000000..cd241447a --- /dev/null +++ b/photon-core/src/main/java/org/photonvision/common/hardware/metrics/SystemMonitorRaspberryPi.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonvision.common.hardware.metrics; + +import java.io.IOException; +import org.photonvision.common.util.ShellExec; + +public class SystemMonitorRaspberryPi extends SystemMonitor { + private final ShellExec runCommand = new ShellExec(true, true); + + @Override + public String getCpuThrottleReason() { + int state = 0; + String output = vcgencmd("get_throttled"); + try { + state = Integer.decode(output); + } catch (NumberFormatException e) { + logger.warn("Could not parse return value: " + output); + } + if ((state & 0x01) != 0) { + return "LOW VOLTAGE"; + } else if ((state & 0x08) != 0) { + return "HIGH TEMP"; + } else if ((state & 0x10000) != 0) { + return "Prev. Low Voltage"; + } else if ((state & 0x80000) != 0) { + return "Prev. High Temp"; + } + return "None"; + } + + @Override + public double getGpuMem() { + String output = vcgencmd("get_mem gpu"); + if (!output.isBlank()) { + return Integer.parseInt(output); + } + return -1.0; + } + + @Override + public double getGpuMemUtil() { + String output = vcgencmd("get_mem malloc"); + if (!output.isBlank()) { + return Integer.parseInt(output); + } + return -1.0; + } + + private String vcgencmd(String cmd) { + if (cmd.isBlank()) { + return ""; + } + String command = "vcgencmd " + cmd; + try { + runCommand.executeBashCommand(command, true, false); + if (runCommand.getExitCode() != 0) { + logger.error("Bad response from vcgencmd: " + runCommand.getOutput()); + return ""; + } else { + return runCommand.getOutput().split("=")[1].replaceAll("[^\\d.]$", ""); + } + } catch (IOException e) { + logger.error("Could not run `vcgencmd`!", e); + return ""; + } + } +} diff --git a/photon-core/src/main/java/org/photonvision/common/hardware/metrics/cmds/CmdBase.java b/photon-core/src/main/java/org/photonvision/common/hardware/metrics/cmds/CmdBase.java deleted file mode 100644 index 78ec89edc..000000000 --- a/photon-core/src/main/java/org/photonvision/common/hardware/metrics/cmds/CmdBase.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) Photon Vision. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.photonvision.common.hardware.metrics.cmds; - -import org.photonvision.common.configuration.HardwareConfig; - -public class CmdBase { - // CPU - public String cpuTemperatureCommand = ""; - public String cpuUtilizationCommand = ""; - public String cpuThrottleReasonCmd = ""; - // RAM - public String ramMemCommand = ""; - public String ramUtilCommand = ""; - // GPU - public String gpuMemCommand = ""; - public String gpuMemUtilCommand = ""; - // NPU - public String npuUsageCommand = ""; - // Disk - public String diskUsageCommand = ""; - // Uptime - public String uptimeCommand = ""; - - public void initCmds(HardwareConfig config) { - // default - do nothing - } -} diff --git a/photon-core/src/main/java/org/photonvision/common/hardware/metrics/cmds/LinuxCmds.java b/photon-core/src/main/java/org/photonvision/common/hardware/metrics/cmds/LinuxCmds.java deleted file mode 100644 index 6db4dbce9..000000000 --- a/photon-core/src/main/java/org/photonvision/common/hardware/metrics/cmds/LinuxCmds.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) Photon Vision. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.photonvision.common.hardware.metrics.cmds; - -import org.photonvision.common.configuration.HardwareConfig; - -public class LinuxCmds extends CmdBase { - public void initCmds(HardwareConfig config) { - // TODO: boards have lots of thermal devices. Hard to pick the CPU - - // CPU - cpuUtilizationCommand = - "top -bn1 | grep \"Cpu(s)\" | sed \"s/.*, *\\([0-9.]*\\)%* id.*/\\1/\" | awk '{print 100 - $1}'"; - - // Uptime - uptimeCommand = "cat /proc/uptime | cut -d ' ' -f1"; - - // RAM - ramMemCommand = "free -m | awk 'FNR == 2 {print $2}'"; - ramUtilCommand = "free -m | awk 'FNR == 2 {print $3}'"; - - // Disk - diskUsageCommand = "df ./ --output=pcent | tail -n +2 | tr -d '%'"; - } -} diff --git a/photon-core/src/main/java/org/photonvision/common/hardware/metrics/cmds/PiCmds.java b/photon-core/src/main/java/org/photonvision/common/hardware/metrics/cmds/PiCmds.java deleted file mode 100644 index 57680726e..000000000 --- a/photon-core/src/main/java/org/photonvision/common/hardware/metrics/cmds/PiCmds.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) Photon Vision. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.photonvision.common.hardware.metrics.cmds; - -import org.photonvision.common.configuration.HardwareConfig; - -public class PiCmds extends LinuxCmds { - /** Applies pi-specific commands, ignoring any input configuration */ - public void initCmds(HardwareConfig config) { - super.initCmds(config); - - // CPU - cpuTemperatureCommand = "sed 's/.\\{3\\}$/.&/' /sys/class/thermal/thermal_zone0/temp"; - cpuThrottleReasonCmd = - "if (( $(( $(vcgencmd get_throttled | grep -Eo 0x[0-9a-fA-F]*) & 0x01 )) != 0x00 )); then echo \"LOW VOLTAGE\"; " - + " elif (( $(( $(vcgencmd get_throttled | grep -Eo 0x[0-9a-fA-F]*) & 0x08 )) != 0x00 )); then echo \"HIGH TEMP\"; " - + " elif (( $(( $(vcgencmd get_throttled | grep -Eo 0x[0-9a-fA-F]*) & 0x10000 )) != 0x00 )); then echo \"Prev. Low Voltage\"; " - + " elif (( $(( $(vcgencmd get_throttled | grep -Eo 0x[0-9a-fA-F]*) & 0x80000 )) != 0x00 )); then echo \"Prev. High Temp\"; " - + " else echo \"None\"; fi"; - - // GPU - gpuMemCommand = "vcgencmd get_mem gpu | grep -Eo '[0-9]+'"; - gpuMemUtilCommand = "vcgencmd get_mem malloc | grep -Eo '[0-9]+'"; - } -} diff --git a/photon-core/src/main/java/org/photonvision/common/hardware/metrics/cmds/QCS6490Cmds.java b/photon-core/src/main/java/org/photonvision/common/hardware/metrics/cmds/QCS6490Cmds.java deleted file mode 100644 index a625900e9..000000000 --- a/photon-core/src/main/java/org/photonvision/common/hardware/metrics/cmds/QCS6490Cmds.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) Photon Vision. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.photonvision.common.hardware.metrics.cmds; - -import org.photonvision.common.configuration.HardwareConfig; - -public class QCS6490Cmds extends LinuxCmds { - /** Applies pi-specific commands, ignoring any input configuration */ - public void initCmds(HardwareConfig config) { - super.initCmds(config); - - /* Thermal zone information can be found in /sys/class/thermal/thermal_zone* directories: - * zone/type: Contains the thermal zone type/name (e.g., "acpi", "x86_pkg_temp") - * zone/temp: Current temperature in millidegrees Celsius (divide by 1000 for actual temp) - * zone/policy: Thermal governor policy (e.g., "step_wise", "power_allocator") - * Each thermal_zone* directory represents a different temperature sensor in the system - */ - - cpuTemperatureCommand = - "cat /sys/class/thermal/thermal_zone10/temp | awk '{printf \"%.1f\", $1/1000}'"; - - // TODO: NPU usage, doesn't seem to be in the same place as the opi. We're gonna just wait on QC - // to get back to us on this one. - } -} diff --git a/photon-core/src/main/java/org/photonvision/common/hardware/metrics/proto/DeviceMetricsProto.java b/photon-core/src/main/java/org/photonvision/common/hardware/metrics/proto/DeviceMetricsProto.java index 4033e2dd4..b3071db93 100644 --- a/photon-core/src/main/java/org/photonvision/common/hardware/metrics/proto/DeviceMetricsProto.java +++ b/photon-core/src/main/java/org/photonvision/common/hardware/metrics/proto/DeviceMetricsProto.java @@ -49,9 +49,12 @@ public class DeviceMetricsProto implements Protobuf i).toArray(), new int[] {2, 13}); NativeDeviceFactoryInterface deviceFactory = HardwareManager.configureCustomGPIO(config); assertTrue(deviceFactory instanceof CustomDeviceFactory); diff --git a/photon-core/src/test/java/org/photonvision/hardware/HardwareTest.java b/photon-core/src/test/java/org/photonvision/hardware/HardwareTest.java index 5c0fc7e09..fcff442b7 100644 --- a/photon-core/src/test/java/org/photonvision/hardware/HardwareTest.java +++ b/photon-core/src/test/java/org/photonvision/hardware/HardwareTest.java @@ -27,37 +27,20 @@ import java.io.IOException; import java.util.HashSet; import java.util.List; import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.photonvision.common.LoadJNI; import org.photonvision.common.configuration.HardwareConfig; import org.photonvision.common.hardware.HardwareManager; -import org.photonvision.common.hardware.Platform; import org.photonvision.common.hardware.VisionLED; -import org.photonvision.common.hardware.metrics.MetricsManager; import org.photonvision.common.util.TestUtils; public class HardwareTest { - @Test - public void testHardware() { + @BeforeAll + public static void init() { LoadJNI.loadLibraries(); - MetricsManager mm = new MetricsManager(); - - if (!Platform.isRaspberryPi()) return; - - System.out.println("Testing on platform: " + Platform.getPlatformName()); - - System.out.println("Printing CPU Info:"); - System.out.println("Memory: " + mm.getRamMem() + "MB"); - System.out.println("Temperature: " + mm.getCpuTemp() + "C"); - System.out.println("Utilization: : " + mm.getCpuUtilization() + "%"); - - System.out.println("Printing GPU Info:"); - System.out.println("Memory: " + mm.getGpuMem() + "MB"); - - System.out.println("Printing RAM Info: "); - System.out.println("Used RAM: : " + mm.getRamUtil() + "MB"); } @Test diff --git a/photon-server/src/main/java/org/photonvision/Main.java b/photon-server/src/main/java/org/photonvision/Main.java index 2e8bf4afb..988c7cc94 100644 --- a/photon-server/src/main/java/org/photonvision/Main.java +++ b/photon-server/src/main/java/org/photonvision/Main.java @@ -33,6 +33,7 @@ import org.photonvision.common.hardware.HardwareManager; import org.photonvision.common.hardware.OsImageData; import org.photonvision.common.hardware.PiVersion; import org.photonvision.common.hardware.Platform; +import org.photonvision.common.hardware.metrics.SystemMonitor; import org.photonvision.common.logging.KernelLogLogger; import org.photonvision.common.logging.LogGroup; import org.photonvision.common.logging.LogLevel; @@ -298,6 +299,10 @@ public class Main { System.exit(0); } + logger.debug("Loading SystemMonitor..."); + SystemMonitor.getInstance().logSystemInformation(); + SystemMonitor.getInstance().startMonitor(500, 1000); + // todo - should test mode just add test mode sources, but still allow local usb cameras to be // added? if (!isTestMode) { diff --git a/photon-server/src/main/java/org/photonvision/server/RequestHandler.java b/photon-server/src/main/java/org/photonvision/server/RequestHandler.java index b6d09e17b..688b02b15 100644 --- a/photon-server/src/main/java/org/photonvision/server/RequestHandler.java +++ b/photon-server/src/main/java/org/photonvision/server/RequestHandler.java @@ -994,11 +994,6 @@ public class RequestHandler { } } - public static void onMetricsPublishRequest(Context ctx) { - HardwareManager.getInstance().publishMetrics(); - ctx.status(204); - } - /** * Get the calibration JSON for a specific observation. Excludes camera image data * diff --git a/photon-server/src/main/java/org/photonvision/server/Server.java b/photon-server/src/main/java/org/photonvision/server/Server.java index fd0f164e7..1481a32e5 100644 --- a/photon-server/src/main/java/org/photonvision/server/Server.java +++ b/photon-server/src/main/java/org/photonvision/server/Server.java @@ -135,7 +135,6 @@ public class Server { app.get("/api/utils/photonvision-journalctl.txt", RequestHandler::onLogExportRequest); app.post("/api/utils/restartProgram", RequestHandler::onProgramRestartRequest); app.post("/api/utils/restartDevice", RequestHandler::onDeviceRestartRequest); - app.post("/api/utils/publishMetrics", RequestHandler::onMetricsPublishRequest); app.get("/api/utils/getImageSnapshots", RequestHandler::onImageSnapshotsRequest); app.get("/api/utils/getCalSnapshot", RequestHandler::onCalibrationSnapshotRequest); app.get("/api/utils/getCalibrationJSON", RequestHandler::onCalibrationExportRequest); diff --git a/photon-targeting/src/main/proto/photon.proto b/photon-targeting/src/main/proto/photon.proto index 0ff3991f9..dea75aab0 100644 --- a/photon-targeting/src/main/proto/photon.proto +++ b/photon-targeting/src/main/proto/photon.proto @@ -81,4 +81,7 @@ message ProtobufDeviceMetrics { repeated double npu_usage = 9; string ip_address = 10; double uptime = 11; + double sent_bit_rate = 12; + double recv_bit_rate = 13; + double disk_usable_space = 14; }