Skip to content

[CSDM-1188][feat] Plugin to monitor and control the camera cooling#3367

Open
pieleric wants to merge 1 commit intodelmic:masterfrom
pieleric:feat-plugin-to-monitor-and-control-the-camera-cooling
Open

[CSDM-1188][feat] Plugin to monitor and control the camera cooling#3367
pieleric wants to merge 1 commit intodelmic:masterfrom
pieleric:feat-plugin-to-monitor-and-control-the-camera-cooling

Conversation

@pieleric
Copy link
Member

@pieleric pieleric commented Feb 13, 2026

Provides an extra menu entry "Acquisition/Camera temperature..." which opens a
small window to monitor and control the camera cooling.

image

Copilot AI review requested due to automatic review settings February 13, 2026 17:32
@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

📝 Walkthrough

Walkthrough

Adds a CameraTemperaturePlugin and a TemperatureControlDialog providing an in-GUI camera temperature control. The plugin detects cameras that expose temperature and targetTemperature VAs, computes a cooling range, registers an "Acquisition/Camera temperature..." menu entry, and opens a single dialog instance. The dialog subscribes to the camera VAs for live updates, displays current temperature, exposes a cooling toggle and target-temperature spin box, writes changes to the camera's targetTemperature VA, and persists/restores a previous target when toggling cooling. VA subscriptions are cleaned up on close.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant App as MainApp/Menu
    participant Plugin as CameraTemperaturePlugin
    participant Dialog as TemperatureControlDialog
    participant VA as Camera VAs

    User->>App: Select "Acquisition/Camera temperature..."
    App->>Plugin: Open dialog request
    Plugin->>Dialog: create or focus single instance
    Dialog->>VA: subscribe to temperature & targetTemperature
    VA-->>Dialog: push current temperature & targetTemperature
    Dialog-->>User: update UI (temperature display, spin box, toggle)

    User->>Dialog: toggle cooling ON
    Dialog->>Dialog: restore previous target or use default
    Dialog->>VA: write new targetTemperature
    VA-->>Dialog: confirm updated targetTemperature
    Dialog-->>User: reflect updated target

    User->>Dialog: change target spin box
    Dialog->>VA: write targetTemperature (only if cooling ON)
    VA-->>Dialog: push updated targetTemperature
    Dialog-->>User: update display

    User->>Dialog: toggle cooling OFF
    Dialog->>Dialog: store current target as previous
    Dialog->>VA: write COOLING_OFF_TEMP as targetTemperature
    VA-->>Dialog: push updated targetTemperature
    Dialog-->>User: reflect cooling OFF state

    User->>Dialog: close
    Dialog->>VA: unsubscribe from temperature & targetTemperature
    Dialog->>Plugin: dialog closed
Loading
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main feature being added: a plugin for monitoring and controlling camera cooling, which matches the changeset that adds the camera_temperature.py plugin.
Description check ✅ Passed The description accurately relates to the changeset by explaining the new menu entry and dialog for camera temperature control that is implemented in the plugin.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@plugins/camera_temperature.py`:
- Around line 55-72: The metadata-default block accesses
self._camera.targetTemperature.value before verifying the VA exists, causing an
AttributeError; move the entire block that reads metadata, checks
MD_TARGET_COOLING_TEMP, reads target_temp and calls updateMetadata (using
MD_TARGET_COOLING_TEMP, COOLING_OFF_TEMP, self._camera.getMetadata(),
self._camera.targetTemperature.value, and self._camera.updateMetadata()) to
after the two model.hasVA(self._camera, "temperature") and
model.hasVA(self._camera, "targetTemperature") guards so targetTemperature is
only accessed once its VA presence is confirmed.
- Around line 197-206: The VA callback _on_temperature_update can run after the
dialog is destroyed because call_in_wx_main uses wx.CallAfter; add a simple
guard: introduce an _is_closed boolean on the dialog (default False), set
_is_closed = True in _on_close before calling Destroy(), and at the start of
_on_temperature_update return immediately if self._is_closed (or if
self._temp_display is None) to avoid touching destroyed widgets; keep using
call_in_wx_main but ensure the early-return guard prevents access to
_temp_display after close.
🧹 Nitpick comments (1)
plugins/camera_temperature.py (1)

229-255: FIXME on line 242: target cooling temp initialization.

The comment acknowledges the issue — the previous target should be captured at init time rather than relying solely on metadata that may not exist yet. This is partially addressed by the metadata initialization in __init__ of the plugin, but if the plugin starts with cooling already off (target ≥ 25°C), no value gets stored, and DEFAULT_COOLING_TEMP is used as the fallback, which may not match the user's preference.

Would you like me to open an issue to track resolving this FIXME, or generate a fix that captures and stores the initial target temperature more robustly?

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new GUI plugin that exposes a dedicated menu entry to monitor and control camera cooling via the camera’s temperature / targetTemperature vigilant attributes.

Changes:

  • Introduces CameraTemperaturePlugin to add the menu entry Acquisition/Camera temperature....
  • Implements a non-modal wx.Dialog that subscribes to live temperature/target updates and allows toggling cooling + setting a target temperature.
  • Persists/restores the last cooling target temperature via camera metadata.

@pieleric pieleric force-pushed the feat-plugin-to-monitor-and-control-the-camera-cooling branch from 5ca23a8 to e1838bb Compare February 16, 2026 08:26
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@plugins/camera_temperature.py`:
- Around line 278-289: Remove the dialog-owned clearing of the plugin reference
from TemperatureControlDialog._on_close (delete the line that sets self._dlg =
None) and move plugin cleanup into CameraTemperaturePlugin: when creating the
dialog, bind the dialog close/destroy event to a new plugin method (e.g.,
CameraTemperaturePlugin._on_dialog_closed) that sets
CameraTemperaturePlugin._dlg = None and lets the event continue, and update
CameraTemperaturePlugin._on_menu_item to defensively check the dialog before
calling IsShown (e.g., ensure self._dlg is not None and hasattr(self._dlg,
"IsShown") or wrap IsShown in a try/except) so the plugin never calls IsShown on
a destroyed wx.Dialog.
🧹 Nitpick comments (1)
plugins/camera_temperature.py (1)

104-110: Docstring is missing the temp_cooling_off parameter.

The __init__ docstring documents parent and camera but omits the temp_cooling_off parameter.

📝 Proposed fix
         """
         Initialize the temperature control dialog.
 
         :param parent: Parent window
         :param camera: Camera component with temperature and targetTemperature VAs
+        :param temp_cooling_off: Temperature value that indicates cooling is off
         """

@pieleric pieleric force-pushed the feat-plugin-to-monitor-and-control-the-camera-cooling branch from e1838bb to 92eb884 Compare February 17, 2026 10:30
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
plugins/camera_temperature.py (2)

104-110: Docstring missing temp_cooling_off parameter.

♻️ Suggested fix
         """
         Initialize the temperature control dialog.
 
         :param parent: Parent window
         :param camera: Camera component with temperature and targetTemperature VAs
+        :param temp_cooling_off: Temperature value (°C) that indicates cooling is off
         """
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/camera_temperature.py` around lines 104 - 110, Docstring for __init__
in plugins/camera_temperature.py is missing the temp_cooling_off parameter;
update the method docstring for def __init__(self, parent: wx.Window, camera:
model.HwComponent, temp_cooling_off: float) -> None to include a :param
temp_cooling_off: line describing that value (e.g., temperature threshold or
value used to disable cooling) and its units/type, so callers and maintainers
understand the purpose of temp_cooling_off.

46-47: Missing type hint for main_app and missing docstring for __init__.

Per coding guidelines, all function parameters should have type hints and all functions should have docstrings.

♻️ Suggested fix
-    def __init__(self, microscope: model.Microscope, main_app) -> None:
+    def __init__(self, microscope: model.Microscope, main_app: "wx.App") -> None:
+        """
+        Initialize the camera temperature plugin.
+
+        :param microscope: The microscope model.
+        :param main_app: The main GUI application.
+        """
         super().__init__(microscope, main_app)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/camera_temperature.py` around lines 46 - 47, Add a type hint for the
main_app parameter and a short docstring to the __init__ method: update the
signature of __init__(self, microscope: model.Microscope, main_app) -> None to
include the concrete type for main_app (e.g., main_app: MainApp or the
appropriate class/interface used across the project) and add a one- or two-line
docstring describing what the constructor does and its parameters; modify the
method that calls super().__init__(microscope, main_app) to match the new
signature.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plugins/camera_temperature.py`:
- Around line 245-252: The restored previous_target (from MD_TARGET_COOLING_TEMP
or DEFAULT_COOLING_TEMP) must be clipped to the camera's allowed
targetTemperature.range before assigning to self._camera.targetTemperature.value
and _target_spin.SetValue; fetch the allowed min/max from
self._camera.targetTemperature.range (or equivalent attributes), clamp
previous_target into [min, max], then use the clamped value for
self._camera.targetTemperature.value, _target_spin.SetValue, and the logging
call so no out-of-range value can raise an exception.

---

Duplicate comments:
In `@plugins/camera_temperature.py`:
- Around line 84-96: The plugin retains a reference to a destroyed wx.Dialog
which makes calling self._dlg.IsShown() unsafe; update _on_menu_item to first
check that self._dlg is not destroyed by using self._dlg.IsBeingDeleted() (or
guard with hasattr/validity checks) before calling IsShown(), and ensure the
dialog clears the plugin reference when closed by binding the dialog's
destroy/close event (e.g., in TemperatureControlDialog or when creating it) to a
handler that sets self._dlg = None (or uses EVT_WINDOW_DESTROY/EVT_CLOSE to call
a plugin method like _on_close to clear the reference) so subsequent menu clicks
never call methods on a destroyed native window.
- Around line 203-212: The wx.CallAfter-backed callback _on_temperature_update
can run after the dialog/widgets are destroyed; add a simple guard at the top of
_on_temperature_update (and mirror it in _on_target_temperature_update) to
return early if the dialog or target widget no longer exists. For example, check
that self still exists and that the target widget attribute (self._temp_display
and the target widget used in _on_target_temperature_update) is present/non-None
and not destroyed (e.g., if not getattr(self, "_temp_display", None) or
getattr(self, "_temp_display", "destroyed") == "destroyed": return) before
calling SetLabel; this prevents accessing destroyed widgets when call_in_wx_main
(wx.CallAfter) invokes the callback. Ensure the same pattern is applied to the
_on_target_temperature_update handler.

---

Nitpick comments:
In `@plugins/camera_temperature.py`:
- Around line 104-110: Docstring for __init__ in plugins/camera_temperature.py
is missing the temp_cooling_off parameter; update the method docstring for def
__init__(self, parent: wx.Window, camera: model.HwComponent, temp_cooling_off:
float) -> None to include a :param temp_cooling_off: line describing that value
(e.g., temperature threshold or value used to disable cooling) and its
units/type, so callers and maintainers understand the purpose of
temp_cooling_off.
- Around line 46-47: Add a type hint for the main_app parameter and a short
docstring to the __init__ method: update the signature of __init__(self,
microscope: model.Microscope, main_app) -> None to include the concrete type for
main_app (e.g., main_app: MainApp or the appropriate class/interface used across
the project) and add a one- or two-line docstring describing what the
constructor does and its parameters; modify the method that calls
super().__init__(microscope, main_app) to match the new signature.

@pieleric pieleric changed the title [feat] Plugin to monitor and control the camera cooling [CSDM-1188][feat] Plugin to monitor and control the camera cooling Mar 6, 2026
@pieleric pieleric force-pushed the feat-plugin-to-monitor-and-control-the-camera-cooling branch from 92eb884 to 2b66d34 Compare March 6, 2026 15:54
Provides an extra menu entry "Acquisition/Camera temperature..." which opens a
small window to monitor and control the camera cooling.
@pieleric pieleric force-pushed the feat-plugin-to-monitor-and-control-the-camera-cooling branch from 2b66d34 to 1419570 Compare March 6, 2026 15:55
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (3)
plugins/camera_temperature.py (3)

245-250: ⚠️ Potential issue | 🟡 Minor

Metadata target temperature may be out of VA range.

When restoring previous_target from metadata (line 246), the value is not clipped to the camera's targetTemperature.range. If the stored value falls outside the valid range (e.g., after a camera configuration change), assigning it to the VA on line 250 will raise an exception.

The fallback on line 248 correctly clips DEFAULT_COOLING_TEMP, but the metadata value should also be clipped.

🐛 Suggested fix
         if is_cooling:
             # Turn cooling on: restore previous target or use default
             metadata = self._camera.getMetadata()
             try:
                 previous_target = metadata[MD_TARGET_COOLING_TEMP]
+                previous_target = self._camera.targetTemperature.clip(previous_target)
             except KeyError:
                 previous_target = self._camera.targetTemperature.clip(DEFAULT_COOLING_TEMP)

             self._camera.targetTemperature.value = previous_target
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/camera_temperature.py` around lines 245 - 250, When restoring
previous_target from metadata (MD_TARGET_COOLING_TEMP), ensure the value is
clipped to the camera VA range before assignment to avoid range exceptions;
replace the current try/except so that on success you call
self._camera.targetTemperature.clip(previous_target) (just like the fallback
does for DEFAULT_COOLING_TEMP) and then assign the clipped value to
self._camera.targetTemperature.value.

200-209: ⚠️ Potential issue | 🟡 Minor

VA callback may execute after dialog is destroyed.

The @call_in_wx_main decorator uses wx.CallAfter to queue execution. If the dialog is destroyed between queueing and execution, accessing self._temp_display will fail with a RuntimeError or cause undefined behavior.

Add a guard at the start of the callback to check if the dialog is still valid.

🛡️ Suggested fix
     `@call_in_wx_main`
     def _on_temperature_update(self, temperature: float) -> None:
         """
         Callback for temperature VA updates.
         Updates the current temperature display.

         :param temperature: Current temperature in °C
         """
+        if not self:
+            return
         self._temp_display.SetLabel(f"{temperature:.1f} °C")

Apply the same guard to _on_target_temperature_update:

     `@call_in_wx_main`
     def _on_target_temperature_update(self, target_temp: float) -> None:
         ...
+        if not self:
+            return
+
         if self._updating_ui:
             return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/camera_temperature.py` around lines 200 - 209, The temperature
callback can run after the dialog is destroyed; add an early-return guard at the
top of _on_temperature_update that verifies the dialog/window is still valid
before touching self._temp_display (for example check that the widget exists and
the window is not destroyed/hidden, e.g. if not getattr(self, "GetHandle",
lambda: None)() or not hasattr(self, "_temp_display"): return), and add the
identical guard to _on_target_temperature_update so both VA callbacks skip
updating UI when the dialog has been torn down.

89-92: ⚠️ Potential issue | 🟡 Minor

Calling IsShown() on a destroyed dialog can crash.

After the dialog is closed and destroyed, self._dlg still holds a reference to the destroyed wx.Dialog object. Calling IsShown() on it can raise a RuntimeError or cause undefined behavior.

Consider either clearing the plugin's _dlg reference when the dialog closes (e.g., by binding to wx.EVT_WINDOW_DESTROY), or adding a defensive check.

🛡️ Suggested fix: Add defensive check
     def _on_menu_item(self) -> None:
         """
         Callback for the menu item.
         Opens the temperature control dialog if not already open.
         """
-        if self._dlg and self._dlg.IsShown():
+        # Check if dialog exists and is still valid (not destroyed)
+        try:
+            if self._dlg and self._dlg.IsShown():
+                # Dialog already open, just bring it to front
+                self._dlg.Raise()
+                return
+        except RuntimeError:
+            # Dialog was destroyed, clear reference
+            self._dlg = None
-            # Dialog already open, just bring it to front
-            self._dlg.Raise()
-            return

         # Create and show the dialog
         self._dlg = TemperatureControlDialog(self.main_app.main_frame, self._camera, self._temp_cooling_off)
         self._dlg.Show()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/camera_temperature.py` around lines 89 - 92, self._dlg may reference
a destroyed wx.Dialog, so calling self._dlg.IsShown() can crash; update the code
that checks/raises the dialog (the block using self._dlg, IsShown(), and
Raise()) to either clear _dlg when the dialog is destroyed (bind a handler to
wx.EVT_WINDOW_DESTROY or the dialog's close event and set self._dlg = None) or
perform a defensive check before calling IsShown() (verify self._dlg is not None
and not destroyed via hasattr/IsBeingDeleted or try/except around IsShown()),
ensuring subsequent Raise() is only called on a valid dialog object.
🧹 Nitpick comments (3)
plugins/camera_temperature.py (3)

104-110: Docstring missing temp_cooling_off parameter documentation.

The docstring documents parent and camera but omits the temp_cooling_off parameter.

📝 Suggested fix
         """
         Initialize the temperature control dialog.

         :param parent: Parent window
         :param camera: Camera component with temperature and targetTemperature VAs
+        :param temp_cooling_off: Temperature threshold indicating cooling is off
         """
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/camera_temperature.py` around lines 104 - 110, The __init__ docstring
for Camera temperature control is missing documentation for the temp_cooling_off
parameter; update the __init__ docstring to add a brief description of
temp_cooling_off (its type float and what it represents, e.g., the temperature
threshold in Celsius at which cooling is turned off) alongside the existing
parent and camera descriptions so callers reading plugins/camera_temperature.py
(specifically the __init__ method) understand the parameter purpose and units.

46-46: Missing type hint for main_app parameter.

The main_app parameter lacks a type annotation. As per coding guidelines, "Always use type hints for function parameters and return types in Python code."

♻️ Suggested fix
-    def __init__(self, microscope: model.Microscope, main_app) -> None:
+    def __init__(self, microscope: model.Microscope, main_app: "odemis.gui.main.OdemisGUIApp") -> None:

Alternatively, if the exact type is not importable without circular dependencies, use a string annotation or typing.Any.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/camera_temperature.py` at line 46, The __init__ method of the
CameraTemperature plugin is missing a type annotation for the main_app
parameter; update the signature of __init__(self, microscope: model.Microscope,
main_app) to add a type for main_app (e.g. use an explicit app class name if
available, a forward-reference string, or typing.Any) to satisfy type-hinting
guidelines, and import typing.Any if you choose that approach; ensure only the
parameter annotation is added (no behavioral changes) in the __init__ method of
the class containing this constructor.

30-30: Unused import: util is imported but never used.

The util module is imported alongside model but is not referenced anywhere in this file.

♻️ Suggested fix
-from odemis import model, util
+from odemis import model
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/camera_temperature.py` at line 30, Remove the unused import by
deleting `util` from the import statement that currently reads `from odemis
import model, util`; keep `model` if it's used elsewhere in this file (e.g., by
functions or classes that reference `model`) so the import becomes `from odemis
import model` to eliminate the unused `util` symbol.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@plugins/camera_temperature.py`:
- Around line 245-250: When restoring previous_target from metadata
(MD_TARGET_COOLING_TEMP), ensure the value is clipped to the camera VA range
before assignment to avoid range exceptions; replace the current try/except so
that on success you call self._camera.targetTemperature.clip(previous_target)
(just like the fallback does for DEFAULT_COOLING_TEMP) and then assign the
clipped value to self._camera.targetTemperature.value.
- Around line 200-209: The temperature callback can run after the dialog is
destroyed; add an early-return guard at the top of _on_temperature_update that
verifies the dialog/window is still valid before touching self._temp_display
(for example check that the widget exists and the window is not
destroyed/hidden, e.g. if not getattr(self, "GetHandle", lambda: None)() or not
hasattr(self, "_temp_display"): return), and add the identical guard to
_on_target_temperature_update so both VA callbacks skip updating UI when the
dialog has been torn down.
- Around line 89-92: self._dlg may reference a destroyed wx.Dialog, so calling
self._dlg.IsShown() can crash; update the code that checks/raises the dialog
(the block using self._dlg, IsShown(), and Raise()) to either clear _dlg when
the dialog is destroyed (bind a handler to wx.EVT_WINDOW_DESTROY or the dialog's
close event and set self._dlg = None) or perform a defensive check before
calling IsShown() (verify self._dlg is not None and not destroyed via
hasattr/IsBeingDeleted or try/except around IsShown()), ensuring subsequent
Raise() is only called on a valid dialog object.

---

Nitpick comments:
In `@plugins/camera_temperature.py`:
- Around line 104-110: The __init__ docstring for Camera temperature control is
missing documentation for the temp_cooling_off parameter; update the __init__
docstring to add a brief description of temp_cooling_off (its type float and
what it represents, e.g., the temperature threshold in Celsius at which cooling
is turned off) alongside the existing parent and camera descriptions so callers
reading plugins/camera_temperature.py (specifically the __init__ method)
understand the parameter purpose and units.
- Line 46: The __init__ method of the CameraTemperature plugin is missing a type
annotation for the main_app parameter; update the signature of __init__(self,
microscope: model.Microscope, main_app) to add a type for main_app (e.g. use an
explicit app class name if available, a forward-reference string, or typing.Any)
to satisfy type-hinting guidelines, and import typing.Any if you choose that
approach; ensure only the parameter annotation is added (no behavioral changes)
in the __init__ method of the class containing this constructor.
- Line 30: Remove the unused import by deleting `util` from the import statement
that currently reads `from odemis import model, util`; keep `model` if it's used
elsewhere in this file (e.g., by functions or classes that reference `model`) so
the import becomes `from odemis import model` to eliminate the unused `util`
symbol.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dbce3631-c426-4391-8f08-0c11705712ac

📥 Commits

Reviewing files that changed from the base of the PR and between 92eb884 and 1419570.

📒 Files selected for processing (1)
  • plugins/camera_temperature.py

@pieleric pieleric requested a review from nandishjpatel March 9, 2026 20:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants