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;
}