diff --git a/photon-client/src/stores/settings/CameraSettingsStore.ts b/photon-client/src/stores/settings/CameraSettingsStore.ts index a9ce46035..4382e4419 100644 --- a/photon-client/src/stores/settings/CameraSettingsStore.ts +++ b/photon-client/src/stores/settings/CameraSettingsStore.ts @@ -91,6 +91,9 @@ export const useCameraSettingsStore = defineStore("cameraSettings", { fpsLimit(): number { return this.currentCameraSettings.fpsLimit; }, + isEnabled(): boolean { + return this.currentCameraSettings.isEnabled; + }, isConnected(): boolean { return this.currentCameraSettings.isConnected; }, @@ -142,6 +145,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", { maxWhiteBalanceTemp: d.maxWhiteBalanceTemp, matchedCameraInfo: d.matchedCameraInfo, fpsLimit: d.fpsLimit, + isEnabled: d.isEnabled, isConnected: d.isConnected, hasConnected: d.hasConnected, mismatch: d.mismatch diff --git a/photon-client/src/types/SettingTypes.ts b/photon-client/src/types/SettingTypes.ts index 291f812ab..724e6afd5 100644 --- a/photon-client/src/types/SettingTypes.ts +++ b/photon-client/src/types/SettingTypes.ts @@ -276,6 +276,7 @@ export interface UiCameraConfiguration { maxWhiteBalanceTemp: number; fpsLimit: number; + isEnabled: boolean; matchedCameraInfo: PVCameraInfo; isConnected: boolean; @@ -447,6 +448,7 @@ export const PlaceholderCameraSettings: UiCameraConfiguration = reactive({ PVUsbCameraInfo: undefined }, fpsLimit: -1, + isEnabled: true, isConnected: true, hasConnected: true, mismatch: false diff --git a/photon-client/src/types/WebsocketDataTypes.ts b/photon-client/src/types/WebsocketDataTypes.ts index d1f18bc29..bbddc5593 100644 --- a/photon-client/src/types/WebsocketDataTypes.ts +++ b/photon-client/src/types/WebsocketDataTypes.ts @@ -68,6 +68,7 @@ export interface WebsocketCameraSettingsUpdate { maxWhiteBalanceTemp: number; matchedCameraInfo: PVCameraInfo; fpsLimit: number; + isEnabled: boolean; isConnected: boolean; hasConnected: boolean; mismatch: boolean; diff --git a/photon-client/src/views/DashboardView.vue b/photon-client/src/views/DashboardView.vue index 20985a00e..c059bcf7f 100644 --- a/photon-client/src/views/DashboardView.vue +++ b/photon-client/src/views/DashboardView.vue @@ -77,8 +77,18 @@ const conflictingCameraShown = computed(() => { return useSettingsStore().general.conflictingCameras.length > 0; }); -const fpsLimitWarningShown = computed(() => { - return Object.values(useCameraSettingsStore().cameras).some((c) => c.fpsLimit > 0); +const fpsLimitedCameras = computed(() => { + return Object.values(useCameraSettingsStore().cameras) + .filter((c) => c.fpsLimit > 0) + .map((c) => c.nickname) + .join(", "); +}); + +const disabledCameras = computed(() => { + return Object.values(useCameraSettingsStore().cameras) + .filter((c) => !c.isEnabled) + .map((c) => c.nickname) + .join(", "); }); const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfiguration); @@ -111,7 +121,7 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat One or more cameras have an FPS limit set! This may cause performance issues. Check your logs for more + >{{ fpsLimitedCameras }} have an FPS limit set! This may cause performance issues. Check your logs for more information. + + {{ disabledCameras }} are disabled! This may cause performance issues. Check your logs for more information. + + fpsLimitConsumer; private final Supplier fpsLimitSupplier; + NTDataChangeListener isEnabledListener; + private final Consumer isEnabledConsumer; + private final BooleanSupplier enabledSupplier; + public NTDataPublisher( String cameraNickname, Supplier pipelineIndexSupplier, @@ -62,13 +66,17 @@ public class NTDataPublisher implements CVPipelineResultConsumer { BooleanSupplier driverModeSupplier, Consumer driverModeConsumer, Supplier fpsLimitSupplier, - Consumer fpsLimitConsumer) { + Consumer fpsLimitConsumer, + BooleanSupplier enabledSupplier, + Consumer isEnabledConsumer) { this.pipelineIndexSupplier = pipelineIndexSupplier; this.pipelineIndexConsumer = pipelineIndexConsumer; this.driverModeSupplier = driverModeSupplier; this.driverModeConsumer = driverModeConsumer; this.fpsLimitSupplier = fpsLimitSupplier; this.fpsLimitConsumer = fpsLimitConsumer; + this.enabledSupplier = enabledSupplier; + this.isEnabledConsumer = isEnabledConsumer; updateCameraNickname(cameraNickname); updateEntries(); @@ -124,6 +132,19 @@ public class NTDataPublisher implements CVPipelineResultConsumer { logger.debug("Set FPS limit to " + newFPSLimit); } + private void onEnabledChange(NetworkTableEvent entryNotification) { + var newEnabled = entryNotification.valueData.value.getBoolean(); + var originalEnabled = enabledSupplier.getAsBoolean(); + + if (newEnabled == originalEnabled) { + logger.debug("Enabled value is already " + newEnabled); + return; + } + + isEnabledConsumer.accept(newEnabled); + logger.debug("Set is enabled to " + newEnabled); + } + private void removeEntries() { if (pipelineIndexListener != null) pipelineIndexListener.remove(); if (driverModeListener != null) driverModeListener.remove(); @@ -148,6 +169,10 @@ public class NTDataPublisher implements CVPipelineResultConsumer { fpsLimitListener = new NTDataChangeListener( ts.subTable.getInstance(), ts.fpsLimitSubscriber, this::onFPSLimitChange); + + isEnabledListener = + new NTDataChangeListener( + ts.subTable.getInstance(), ts.enabledSubscriber, this::onEnabledChange); } public void updateCameraNickname(String newCameraNickname) { @@ -198,6 +223,7 @@ public class NTDataPublisher implements CVPipelineResultConsumer { ts.pipelineIndexPublisher.set(pipelineIndexSupplier.get()); ts.driverModePublisher.set(driverModeSupplier.getAsBoolean()); ts.fpsLimitPublisher.set(fpsLimitSupplier.get()); + ts.enabledPublisher.set(enabledSupplier.getAsBoolean()); ts.latencyMillisEntry.set(acceptedResult.getLatencyMillis()); ts.fpsEntry.set(acceptedResult.fps); ts.hasTargetEntry.set(acceptedResult.hasTargets()); diff --git a/photon-core/src/main/java/org/photonvision/common/dataflow/websocket/UICameraConfiguration.java b/photon-core/src/main/java/org/photonvision/common/dataflow/websocket/UICameraConfiguration.java index 617822c85..528889884 100644 --- a/photon-core/src/main/java/org/photonvision/common/dataflow/websocket/UICameraConfiguration.java +++ b/photon-core/src/main/java/org/photonvision/common/dataflow/websocket/UICameraConfiguration.java @@ -55,6 +55,7 @@ public class UICameraConfiguration { public boolean mismatch; public int fpsLimit; + public boolean isEnabled; // Status for if the underlying device is present and such public boolean isConnected; diff --git a/photon-core/src/main/java/org/photonvision/vision/processes/VisionModule.java b/photon-core/src/main/java/org/photonvision/vision/processes/VisionModule.java index d9afb714a..bc57316ac 100644 --- a/photon-core/src/main/java/org/photonvision/vision/processes/VisionModule.java +++ b/photon-core/src/main/java/org/photonvision/vision/processes/VisionModule.java @@ -88,6 +88,7 @@ public class VisionModule { private int outputStreamPort = -1; private int fpsLimit = -1; + private boolean enabled = true; FileSaveFrameConsumer inputFrameSaver; FileSaveFrameConsumer outputFrameSaver; @@ -137,7 +138,8 @@ public class VisionModule { this::consumeResult, this.cameraQuirks, getChangeSubscriber(), - this::getFPSLimit); + this::getFPSLimit, + this::getEnabled); this.streamRunnable = new StreamRunnable(new OutputStreamPipeline()); changeSubscriberHandle = DataChangeService.getInstance().addSubscriber(changeSubscriber); @@ -153,7 +155,9 @@ public class VisionModule { pipelineManager::getDriverMode, this::setDriverMode, this::getFPSLimit, - this::setFPSLimit); + this::setFPSLimit, + this::getEnabled, + this::setEnabled); uiDataConsumer = new UIDataPublisher(visionSource.getSettables().getConfiguration().uniqueName); statusLEDsConsumer = new StatusLEDConsumer(visionSource.getSettables().getConfiguration().uniqueName); @@ -645,6 +649,25 @@ public class VisionModule { return fpsLimit; } + /** + * Sets whether the camera is enabled/disabled, disabling the camera allows you to reduce util + * while keeping the vision runner up for fast toggling. + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + saveAndBroadcastAll(); + } + + /** + * Gets whether the camera is enabled or disabled, if disabled the vision runner will still be + * running but the camera will not capture frames, allowing for fast toggling. + * + * @return the enabled state of the camera + */ + public boolean getEnabled() { + return enabled; + } + public CameraConfiguration getStateAsCameraConfig() { var config = visionSource.getSettables().getConfiguration(); config.setPipelineSettings(pipelineManager.userPipelineSettings); diff --git a/photon-core/src/main/java/org/photonvision/vision/processes/VisionRunner.java b/photon-core/src/main/java/org/photonvision/vision/processes/VisionRunner.java index f1b99d6fd..e45e15066 100644 --- a/photon-core/src/main/java/org/photonvision/vision/processes/VisionRunner.java +++ b/photon-core/src/main/java/org/photonvision/vision/processes/VisionRunner.java @@ -50,6 +50,7 @@ public class VisionRunner { private final List runnableList = new ArrayList(); private final QuirkyCamera cameraQuirks; private final Supplier fpsLimitSupplier; + private final Supplier enabledSupplier; private long loopCount; @@ -57,9 +58,14 @@ public class VisionRunner { * VisionRunner contains a thread to run a pipeline, given a frame, and will give the result to * the consumer. * - * @param frameSupplier The supplier of the latest frame. - * @param pipelineSupplier The supplier of the current pipeline. - * @param pipelineResultConsumer The consumer of the latest result. + * @param frameSupplier + * @param pipelineSupplier + * @param pipelineResultConsumer + * @param cameraQuirks + * @param changeSubscriber The subscriber to setting changes for this VisionRunner, so it can + * update its settings when they change. + * @param fpsLimitSupplier + * @param enabledSupplier */ public VisionRunner( FrameProvider frameSupplier, @@ -67,13 +73,15 @@ public class VisionRunner { Consumer pipelineResultConsumer, QuirkyCamera cameraQuirks, VisionModuleChangeSubscriber changeSubscriber, - Supplier fpsLimitSupplier) { + Supplier fpsLimitSupplier, + Supplier enabledSupplier) { this.frameSupplier = frameSupplier; this.pipelineSupplier = pipelineSupplier; this.pipelineResultConsumer = pipelineResultConsumer; this.cameraQuirks = cameraQuirks; this.changeSubscriber = changeSubscriber; this.fpsLimitSupplier = fpsLimitSupplier; + this.enabledSupplier = enabledSupplier; visionProcessThread = new Thread(this::update); visionProcessThread.setName("VisionRunner - " + frameSupplier.getName()); @@ -130,6 +138,28 @@ public class VisionRunner { return future; } + /** + * Waits until the next time this VisionRunner should run its pipeline, based on current FPS limit + */ + private void waitUntilNextTick(long start) { + int fpsLimit = fpsLimitSupplier.get(); + + if (fpsLimit > 0) { + long sleepTime = (long) (1000 / fpsLimit - (System.currentTimeMillis() - start)); + + if (sleepTime > 0) { + try { + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + } + } + return; + } else { + // Fall through to no limit + return; + } + } + private void update() { // wait for the camera to connect while (!frameSupplier.isConnected() && !Thread.interrupted()) { @@ -194,11 +224,23 @@ public class VisionRunner { frame.release(); pipelineResultConsumer.accept(new CVPipelineResult(0l, 0, 0, null, new Frame())); } else if (pipeline == pipelineSupplier.get()) { + if (!enabledSupplier.get()) { + // If we are skipping processing due to the camera being disabled, we still want to send a + // result with the new frame and settings, just with a null pipeline result + pipelineResultConsumer.accept(new CVPipelineResult(0l, 0, 0, null, new Frame())); + frame.release(); + continue; + } + // If the pipeline has changed while we are getting our frame we should scrap // that frame it may result in incorrect frame settings like hsv values // There's no guarantee the processing type change will occur this tick, so // pipelines should check themselves + + // If we have an FPS limit, check if it's 0, in which case we skip processing and just send + // a blank frame, otherwise we sleep until the next tick + waitUntilNextTick(start); try { var pipelineResult = pipeline.run(frame, cameraQuirks); pipelineResultConsumer.accept(pipelineResult); @@ -207,18 +249,6 @@ public class VisionRunner { } loopCount++; } - int fpsLimit = fpsLimitSupplier.get(); - if (fpsLimit > 0) { - long sleepTime = (long) (1000 / fpsLimit - (System.currentTimeMillis() - start)); - - if (sleepTime > 0) { - try { - Thread.sleep(sleepTime); - } catch (InterruptedException e) { - return; - } - } - } } } } diff --git a/photon-lib/py/photonlibpy/networktables/NTTopicSet.py b/photon-lib/py/photonlibpy/networktables/NTTopicSet.py index f8294955e..242212e9e 100644 --- a/photon-lib/py/photonlibpy/networktables/NTTopicSet.py +++ b/photon-lib/py/photonlibpy/networktables/NTTopicSet.py @@ -54,6 +54,13 @@ class NTTopicSet: self.fpsLimitSubscriber.getTopic().publish().setDefault(-1) + self.enabledPublisher = self.subTable.getBooleanTopic("enabled").publish() + self.enabledSubscriber = self.subTable.getBooleanTopic( + "enabledRequest" + ).subscribe(True) + + self.enabledSubscriber.getTopic().publish().setDefault(True) + self.latencyMillisEntry = self.subTable.getDoubleTopic( "latencyMillis" ).publish() diff --git a/photon-lib/py/photonlibpy/photonCamera.py b/photon-lib/py/photonlibpy/photonCamera.py index 653afafa7..cd792d763 100644 --- a/photon-lib/py/photonlibpy/photonCamera.py +++ b/photon-lib/py/photonlibpy/photonCamera.py @@ -80,6 +80,12 @@ class PhotonCamera: self._fpsLimitSubscriber = self._cameraTable.getIntegerTopic( "fpsLimit" ).subscribe(-1) + self._enabledPublisher = self._cameraTable.getBooleanTopic( + "enabledRequest" + ).publish() + self._enabledSubscriber = self._cameraTable.getBooleanTopic( + "enabled" + ).subscribe(True) self._inputSaveImgEntry = self._cameraTable.getIntegerTopic( "inputSaveImgCmd" ).getEntry(0) @@ -204,11 +210,28 @@ class PhotonCamera: def setFPSLimit(self, fpsLimit: int) -> None: """Sets the FPS limit on the camera. - :param fpsLimit: The FPS limit to set. Set to -1 for unlimited FPS. +

A negative FPS limit is treated as no FPS limit, and will run as fast as possible. + +

Otherwise, will limit processing to at most the provided FPS limit + + :param fpsLimit: The FPS limit to set. """ self._fpsLimitPublisher.set(fpsLimit) + def getEnabled(self) -> bool: + """:returns: Whether the camera is enabled.""" + + return self._enabledSubscriber.get() + + def setEnabled(self, enabled: bool) -> None: + """Sets whether the camera is enabled, default is true. + + :param enabled: Whether to enable the camera. + """ + + self._enabledPublisher.set(enabled) + def takeInputSnapshot(self) -> None: """Request the camera to save a new image file from the input camera stream with overlays. Images take up space in the filesystem of the PhotonCamera. Calling it frequently will fill up disk diff --git a/photon-lib/src/main/java/org/photonvision/PhotonCamera.java b/photon-lib/src/main/java/org/photonvision/PhotonCamera.java index c16fa36f4..c07b6fa74 100644 --- a/photon-lib/src/main/java/org/photonvision/PhotonCamera.java +++ b/photon-lib/src/main/java/org/photonvision/PhotonCamera.java @@ -65,6 +65,8 @@ public class PhotonCamera implements AutoCloseable { BooleanSubscriber driverModeSubscriber; IntegerPublisher fpsLimitPublisher; IntegerSubscriber fpsLimitSubscriber; + BooleanSubscriber enabledSubscriber; + BooleanPublisher enabledPublisher; StringSubscriber versionEntry; IntegerEntry inputSaveImgEntry, outputSaveImgEntry; IntegerPublisher pipelineIndexRequest, ledModeRequest; @@ -82,6 +84,8 @@ public class PhotonCamera implements AutoCloseable { driverModeSubscriber.close(); fpsLimitPublisher.close(); fpsLimitSubscriber.close(); + enabledPublisher.close(); + enabledSubscriber.close(); versionEntry.close(); inputSaveImgEntry.close(); outputSaveImgEntry.close(); @@ -155,6 +159,8 @@ public class PhotonCamera implements AutoCloseable { driverModeSubscriber = cameraTable.getBooleanTopic("driverMode").subscribe(false); fpsLimitPublisher = cameraTable.getIntegerTopic("fpsLimitRequest").publish(); fpsLimitSubscriber = cameraTable.getIntegerTopic("fpsLimit").subscribe(-1); + enabledPublisher = cameraTable.getBooleanTopic("enabledRequest").publish(); + enabledSubscriber = cameraTable.getBooleanTopic("enabled").subscribe(true); inputSaveImgEntry = cameraTable.getIntegerTopic("inputSaveImgCmd").getEntry(0); outputSaveImgEntry = cameraTable.getIntegerTopic("outputSaveImgCmd").getEntry(0); pipelineIndexRequest = cameraTable.getIntegerTopic("pipelineIndexRequest").publish(); @@ -344,8 +350,6 @@ public class PhotonCamera implements AutoCloseable { } /** - * Gets the FPS limit set on the camera. - * * @return The current FPS limit. */ public int getFPSLimit() { @@ -355,12 +359,32 @@ public class PhotonCamera implements AutoCloseable { /** * Sets the FPS limit on the camera. * - * @param fps The FPS limit to set. Set to -1 for unlimited FPS. + *

A negative FPS limit is treated as no FPS limit, and will run as fast as possible. + * + *

Otherwise, will limit processing to at most the provided FPS limit + * + * @param fps The FPS limit to set. */ public void setFPSLimit(int fps) { fpsLimitPublisher.set(fps); } + /** + * Sets whether the camera is enabled, default is true. + * + * @param enabled Whether to enable the camera. + */ + public void setEnabled(boolean enabled) { + enabledPublisher.set(enabled); + } + + /** + * @return Whether the camera is enabled. + */ + public boolean getEnabled() { + return enabledSubscriber.get(); + } + /** * Request the camera to save a new image file from the input camera stream with overlays. Images * take up space in the filesystem of the PhotonCamera. Calling it frequently will fill up disk diff --git a/photon-lib/src/main/native/cpp/photon/PhotonCamera.cpp b/photon-lib/src/main/native/cpp/photon/PhotonCamera.cpp index cd6970518..a63fe1aaf 100644 --- a/photon-lib/src/main/native/cpp/photon/PhotonCamera.cpp +++ b/photon-lib/src/main/native/cpp/photon/PhotonCamera.cpp @@ -157,6 +157,8 @@ PhotonCamera::PhotonCamera(wpi::nt::NetworkTableInstance instance, fpsLimitSubscriber(rootTable->GetIntegerTopic("fpsLimit").Subscribe(-1)), fpsLimitPublisher( rootTable->GetIntegerTopic("fpsLimitRequest").Publish()), + enabledSubscriber(rootTable->GetBooleanTopic("enabled").Subscribe(true)), + enabledPublisher(rootTable->GetBooleanTopic("enabledRequest").Publish()), heartbeatSubscriber( rootTable->GetIntegerTopic("heartbeat").Subscribe(-1)), topicNameSubscriber(instance, PHOTON_PREFIX, {.topicsOnly = true}), @@ -291,6 +293,10 @@ void PhotonCamera::SetFPSLimit(int fpsLimit) { fpsLimitPublisher.Set(fpsLimit); } +bool PhotonCamera::GetEnabled() const { return enabledSubscriber.Get(); } + +void PhotonCamera::SetEnabled(bool enabled) { enabledPublisher.Set(enabled); } + void PhotonCamera::TakeInputSnapshot() { inputSaveImgEntry.Set(inputSaveImgSubscriber.Get() + 1); } diff --git a/photon-lib/src/main/native/include/photon/PhotonCamera.h b/photon-lib/src/main/native/include/photon/PhotonCamera.h index 1e8a9ca73..4b40d079e 100644 --- a/photon-lib/src/main/native/include/photon/PhotonCamera.h +++ b/photon-lib/src/main/native/include/photon/PhotonCamera.h @@ -104,7 +104,17 @@ class PhotonCamera { bool GetDriverMode() const; /** - * @param fpsLimit The FPS limit to set. Use -1 for unlimited FPS. + * Sets the FPS limit on the camera. + * + *

An FPS of 0 means to pause processing until a FPS limit greater than 0 + * is set. + * + *

A negative FPS limit is treated as no FPS limit, and will run as fast as + * possible. + * + *

Otherwise, will limit processing to at most the provided FPS limit + * + * @param fps The FPS limit to set. */ void SetFPSLimit(int fpsLimit); @@ -113,6 +123,17 @@ class PhotonCamera { */ int GetFPSLimit() const; + /** + * Sets whether the camera is enabled, default is true. + * @param enabled Whether to enable the camera. + */ + void SetEnabled(bool enabled); + + /** + * @return Whether the camera is enabled. + */ + bool GetEnabled() const; + /** * Request the camera to save a new image file from the input * camera stream with overlays. @@ -233,6 +254,8 @@ class PhotonCamera { wpi::nt::BooleanPublisher driverModePublisher; wpi::nt::IntegerSubscriber fpsLimitSubscriber; wpi::nt::IntegerPublisher fpsLimitPublisher; + wpi::nt::BooleanSubscriber enabledSubscriber; + wpi::nt::BooleanPublisher enabledPublisher; wpi::nt::IntegerSubscriber ledModeSubscriber; diff --git a/photon-targeting/src/main/java/org/photonvision/common/networktables/NTTopicSet.java b/photon-targeting/src/main/java/org/photonvision/common/networktables/NTTopicSet.java index 4e7354ce9..fd41670ce 100644 --- a/photon-targeting/src/main/java/org/photonvision/common/networktables/NTTopicSet.java +++ b/photon-targeting/src/main/java/org/photonvision/common/networktables/NTTopicSet.java @@ -57,6 +57,9 @@ public class NTTopicSet { public IntegerPublisher fpsLimitPublisher; public IntegerSubscriber fpsLimitSubscriber; + public BooleanPublisher enabledPublisher; + public BooleanSubscriber enabledSubscriber; + public DoublePublisher latencyMillisEntry; public DoublePublisher fpsEntry; public BooleanPublisher hasTargetEntry; @@ -109,6 +112,11 @@ public class NTTopicSet { fpsLimitSubscriber.getTopic().publish().setDefault(-1); + enabledPublisher = subTable.getBooleanTopic("enabled").publish(); + enabledSubscriber = subTable.getBooleanTopic("enabledRequest").subscribe(true); + + enabledSubscriber.getTopic().publish().setDefault(true); + latencyMillisEntry = subTable.getDoubleTopic("latencyMillis").publish(); fpsEntry = subTable.getDoubleTopic("fps").publish(); hasTargetEntry = subTable.getBooleanTopic("hasTarget").publish(); @@ -141,6 +149,9 @@ public class NTTopicSet { if (fpsLimitPublisher != null) fpsLimitPublisher.close(); if (fpsLimitSubscriber != null) fpsLimitSubscriber.close(); + if (enabledPublisher != null) enabledPublisher.close(); + if (enabledSubscriber != null) enabledSubscriber.close(); + if (latencyMillisEntry != null) latencyMillisEntry.close(); if (fpsEntry != null) fpsEntry.close(); if (hasTargetEntry != null) hasTargetEntry.close();