diff --git a/photon-core/src/main/java/org/photonvision/common/dataflow/networktables/NetworkTablesManager.java b/photon-core/src/main/java/org/photonvision/common/dataflow/networktables/NetworkTablesManager.java index e713edcbb2..bd04dce0d8 100644 --- a/photon-core/src/main/java/org/photonvision/common/dataflow/networktables/NetworkTablesManager.java +++ b/photon-core/src/main/java/org/photonvision/common/dataflow/networktables/NetworkTablesManager.java @@ -41,6 +41,7 @@ import org.photonvision.common.dataflow.events.OutgoingUIEvent; import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration; import org.photonvision.common.hardware.HardwareManager; +import org.photonvision.common.hardware.alerts.AlertGroups; import org.photonvision.common.logging.LogGroup; import org.photonvision.common.logging.LogLevel; import org.photonvision.common.logging.Logger; @@ -66,9 +67,9 @@ public class NetworkTablesManager { new MultiSubscriber(ntInstance, new String[] {kRootTableName + "/" + kCoprocTableName + "/"}); // Creating the alert up here since it should be persistent - private final Alert conflictAlert = new Alert("PhotonAlerts", "", AlertType.kWarning); + private final Alert conflictAlert = new Alert(AlertGroups.PHOTON_ALERTS, "", AlertType.kWarning); - private final Alert mismatchAlert = new Alert("PhotonAlerts", "", AlertType.kWarning); + private final Alert mismatchAlert = new Alert(AlertGroups.PHOTON_ALERTS, "", AlertType.kWarning); public boolean conflictingHostname = false; public String conflictingCameras = ""; diff --git a/photon-core/src/main/java/org/photonvision/common/hardware/alerts/AlertGroups.java b/photon-core/src/main/java/org/photonvision/common/hardware/alerts/AlertGroups.java new file mode 100644 index 0000000000..ffed8ce437 --- /dev/null +++ b/photon-core/src/main/java/org/photonvision/common/hardware/alerts/AlertGroups.java @@ -0,0 +1,25 @@ +/* + * 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.alerts; + +/** Shared alert-group names for WPILib Alert instances. */ +public final class AlertGroups { + public static final String PHOTON_ALERTS = "PhotonAlerts"; + + private AlertGroups() {} +} 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 index a9401649be..9beeea85d5 100644 --- 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 @@ -20,6 +20,9 @@ import edu.wpi.first.cscore.CameraServerJNI; import edu.wpi.first.networktables.NetworkTable; import edu.wpi.first.networktables.ProtobufPublisher; +import edu.wpi.first.wpilibj.Alert; +import edu.wpi.first.wpilibj.Alert.AlertType; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; import java.io.IOException; import java.nio.file.FileStore; import java.nio.file.Files; @@ -32,6 +35,7 @@ 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.alerts.AlertGroups; import org.photonvision.common.logging.LogGroup; import org.photonvision.common.logging.Logger; import org.photonvision.common.networking.NetworkUtils; @@ -62,6 +66,9 @@ private record NetworkTraffic(double sentBitRate, double recvBitRate) {} .getProtobufTopic(CameraServerJNI.getHostname(), DeviceMetrics.proto) .publish(); + private final Alert thermalThrottlingAlert = + new Alert(AlertGroups.PHOTON_ALERTS, "Thermal throttling detected!", AlertType.kWarning); + private SystemInfo si; private CentralProcessor cpu; private OperatingSystem os; @@ -140,11 +147,11 @@ protected SystemMonitor() { } /** - * Returns a comma-separated list of addtional thermal zone types that should be checked to get + * Returns a comma-separated list of additional 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. + * zone with a type that matches 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 coprocessor. * * @return String containing a comma-separated list of thermal zone types for reading CPU * temperature. @@ -159,7 +166,7 @@ protected String getThermalZoneTypes() { /** * Starts the periodic system monitor that publishes performance metrics. The metrics are - * published every millisUpdateInerval seconds after a millisStartDelay startup delay. Calling + * published every millisUpdateInterval 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. * @@ -210,6 +217,9 @@ private void publishMetrics() { metricPublisher.set(metrics); + thermalThrottlingAlert.set(this.isThermallyThrottling()); + SmartDashboard.updateValues(); + if (writeMetricsToLog) { logMetrics(metrics); } @@ -233,7 +243,7 @@ private void logMetrics(DeviceMetrics metrics) { String.format("CPU Throttle: %s, ", metrics.cpuThr().isBlank() ? "N/A" : metrics.cpuThr())); sb.append( String.format( - "Data sent: %.0f Kbps, Data recieved: %.0f Kbps", + "Data sent: %.0f Kbps, Data received: %.0f Kbps", metrics.sentBitRate() / 1000, metrics.recvBitRate() / 1000)); logger.debug(sb.toString()); } @@ -446,6 +456,16 @@ public String getCpuThrottleReason() { return ""; } + /** + * Returns true if the device is currently experiencing thermal throttling. Platforms that support + * thermal throttling detection will override this method. + * + * @return true if thermally throttling, false otherwise. + */ + public boolean isThermallyThrottling() { + return false; + } + /** * Returns the total GPU memory in MiB. * @@ -475,7 +495,7 @@ public String getIpAddress() { } /** - * Returns a NetworkTraffic instance containing the average sent and recieved network traffic + * Returns a NetworkTraffic instance containing the average sent and received 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 @@ -537,7 +557,7 @@ private void testSM() { () -> { var nt = getNetworkTraffic(); return String.format( - "Data sent: %.0f Kbps, Data recieved: %.0f Kbps", + "Data sent: %.0f Kbps, Data received: %.0f Kbps", nt.sentBitRate() / 1000, nt.recvBitRate() / 1000); }); 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 index cd241447aa..4ce4e91fd1 100644 --- 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 @@ -25,13 +25,7 @@ public class SystemMonitorRaspberryPi extends SystemMonitor { @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); - } + int state = getThrottleState(); if ((state & 0x01) != 0) { return "LOW VOLTAGE"; } else if ((state & 0x08) != 0) { @@ -44,6 +38,22 @@ public String getCpuThrottleReason() { return "None"; } + @Override + public boolean isThermallyThrottling() { + return (getThrottleState() & 0x08) != 0; + } + + private int getThrottleState() { + int state = 0; + String output = vcgencmd("get_throttled"); + try { + state = Integer.decode(output); + } catch (NumberFormatException e) { + logger.warn("Could not parse return value: " + output); + } + return state; + } + @Override public double getGpuMem() { String output = vcgencmd("get_mem gpu"); diff --git a/photon-lib/src/main/java/org/photonvision/AlertGroups.java b/photon-lib/src/main/java/org/photonvision/AlertGroups.java new file mode 100644 index 0000000000..fbbb1f2b5e --- /dev/null +++ b/photon-lib/src/main/java/org/photonvision/AlertGroups.java @@ -0,0 +1,32 @@ +/* + * MIT License + * + * Copyright (c) PhotonVision + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.photonvision; + +/** Shared alert-group names for WPILib Alert instances. */ +public final class AlertGroups { + public static final String PHOTON_ALERTS = "PhotonAlerts"; + + private AlertGroups() {} +} diff --git a/photon-lib/src/main/java/org/photonvision/PhotonCamera.java b/photon-lib/src/main/java/org/photonvision/PhotonCamera.java index d548e17f09..c0b2f6efaf 100644 --- a/photon-lib/src/main/java/org/photonvision/PhotonCamera.java +++ b/photon-lib/src/main/java/org/photonvision/PhotonCamera.java @@ -59,7 +59,6 @@ public class PhotonCamera implements AutoCloseable { private static int InstanceCount = 1; public static final String kTableName = "photonvision"; - private static final String PHOTON_ALERT_GROUP = "PhotonAlerts"; private final NetworkTable cameraTable; PacketSubscriber resultSubscriber; @@ -138,8 +137,10 @@ public PhotonCamera(NetworkTableInstance instance, String cameraName) { name = cameraName; disconnectAlert = new Alert( - PHOTON_ALERT_GROUP, "PhotonCamera '" + name + "' is disconnected.", AlertType.kWarning); - timesyncAlert = new Alert(PHOTON_ALERT_GROUP, "", AlertType.kWarning); + AlertGroups.PHOTON_ALERTS, + "PhotonCamera '" + name + "' is disconnected.", + AlertType.kWarning); + timesyncAlert = new Alert(AlertGroups.PHOTON_ALERTS, "", AlertType.kWarning); rootPhotonTable = instance.getTable(kTableName); this.cameraTable = rootPhotonTable.getSubTable(cameraName); path = cameraTable.getPath(); diff --git a/photon-lib/src/main/native/cpp/photon/PhotonCamera.cpp b/photon-lib/src/main/native/cpp/photon/PhotonCamera.cpp index 1c4833c4bf..9451cd70d1 100644 --- a/photon-lib/src/main/native/cpp/photon/PhotonCamera.cpp +++ b/photon-lib/src/main/native/cpp/photon/PhotonCamera.cpp @@ -39,6 +39,7 @@ #include #include "PhotonVersion.h" +#include "photon/AlertGroups.h" #include "photon/dataflow/structures/Packet.h" static constexpr units::second_t WARN_DEBOUNCE_SEC = 5_s; @@ -109,7 +110,6 @@ namespace photon { constexpr const units::second_t VERSION_CHECK_INTERVAL = 5_s; static const std::vector PHOTON_PREFIX = {"/photonvision/"}; -static const std::string PHOTON_ALERT_GROUP{"PhotonAlerts"}; bool PhotonCamera::VERSION_CHECK_ENABLED = true; void PhotonCamera::SetVersionCheckEnabled(bool enabled) { @@ -160,11 +160,12 @@ PhotonCamera::PhotonCamera(nt::NetworkTableInstance instance, topicNameSubscriber(instance, PHOTON_PREFIX, {.topicsOnly = true}), path(rootTable->GetPath()), cameraName(cameraName), - disconnectAlert(PHOTON_ALERT_GROUP, + disconnectAlert(AlertGroups::kPhotonAlerts, std::string{"PhotonCamera '"} + std::string{cameraName} + "' is disconnected.", frc::Alert::AlertType::kWarning), - timesyncAlert(PHOTON_ALERT_GROUP, "", frc::Alert::AlertType::kWarning) { + timesyncAlert(AlertGroups::kPhotonAlerts, "", + frc::Alert::AlertType::kWarning) { verifyDependencies(); HAL_Report(HALUsageReporting::kResourceType_PhotonCamera, InstanceCount); InstanceCount++; diff --git a/photon-lib/src/main/native/include/photon/AlertGroups.h b/photon-lib/src/main/native/include/photon/AlertGroups.h new file mode 100644 index 0000000000..a57ec95b3e --- /dev/null +++ b/photon-lib/src/main/native/include/photon/AlertGroups.h @@ -0,0 +1,33 @@ +/* + * MIT License + * + * Copyright (c) PhotonVision + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include + +namespace photon::AlertGroups { + +inline constexpr std::string_view kPhotonAlerts = "PhotonAlerts"; + +} // namespace photon::AlertGroups diff --git a/photon-lib/src/test/java/org/photonvision/PhotonCameraTest.java b/photon-lib/src/test/java/org/photonvision/PhotonCameraTest.java index 9a2f69dd42..25e41db73a 100644 --- a/photon-lib/src/test/java/org/photonvision/PhotonCameraTest.java +++ b/photon-lib/src/test/java/org/photonvision/PhotonCameraTest.java @@ -310,6 +310,7 @@ public void testAlerts() throws InterruptedException { // GIVEN a fresh NT instance var cameraName = "foobar"; + var warningAlertKey = AlertGroups.PHOTON_ALERTS + "/warnings"; // AND a photoncamera that is disconnected var camera = new PhotonCamera(inst, cameraName); @@ -327,7 +328,7 @@ public void testAlerts() throws InterruptedException { // The alert state will be set (hard-coded here) assertTrue( - Arrays.stream(SmartDashboard.getStringArray("PhotonAlerts/warnings", new String[0])) + Arrays.stream(SmartDashboard.getStringArray(warningAlertKey, new String[0])) .anyMatch(it -> it.equals(disconnectedCameraString))); Thread.sleep(20); @@ -356,11 +357,11 @@ public void testAlerts() throws InterruptedException { // THEN the camera isn't disconnected assertTrue( - Arrays.stream(SmartDashboard.getStringArray("PhotonAlerts/warnings", new String[0])) + Arrays.stream(SmartDashboard.getStringArray(warningAlertKey, new String[0])) .noneMatch(it -> it.equals(disconnectedCameraString))); // AND the alert string looks like a timesync warning assertTrue( - Arrays.stream(SmartDashboard.getStringArray("PhotonAlerts/warnings", new String[0])) + Arrays.stream(SmartDashboard.getStringArray(warningAlertKey, new String[0])) .filter(it -> it.contains("is not connected to the TimeSyncServer")) .count() == 1); diff --git a/photon-lib/src/test/native/cpp/PhotonCameraTest.cpp b/photon-lib/src/test/native/cpp/PhotonCameraTest.cpp index e021299206..fb417cdb6c 100644 --- a/photon-lib/src/test/native/cpp/PhotonCameraTest.cpp +++ b/photon-lib/src/test/native/cpp/PhotonCameraTest.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include @@ -61,6 +62,8 @@ TEST(TimeSyncProtocolTest, Smoketest) { TEST(PhotonCameraTest, Alerts) { using frc::SmartDashboard; + const std::string warningsAlertKey = + std::string{photon::AlertGroups::kPhotonAlerts} + "/warnings"; // GIVEN a local-only NT instance auto inst = nt::NetworkTableInstance::GetDefault(); @@ -86,7 +89,7 @@ TEST(PhotonCameraTest, Alerts) { SmartDashboard::UpdateValues(); // The alert state will be set (hard-coded here) - auto alerts = SmartDashboard::GetStringArray("PhotonAlerts/warnings", {}); + auto alerts = SmartDashboard::GetStringArray(warningsAlertKey, {}); EXPECT_TRUE( std::any_of(alerts.begin(), alerts.end(), [&disconnectedCameraString](const std::string& alert) { @@ -113,7 +116,7 @@ TEST(PhotonCameraTest, Alerts) { SmartDashboard::UpdateValues(); // THEN the camera isn't disconnected - auto alerts = SmartDashboard::GetStringArray("PhotonAlerts/warnings", {}); + auto alerts = SmartDashboard::GetStringArray(warningsAlertKey, {}); fmt::println("{}:{}: saw alerts: {}", __FILE__, __LINE__, alerts); EXPECT_TRUE( std::none_of(alerts.begin(), alerts.end(),