From 946c9976a6eefc981c303cbd6a6f7f12fef08656 Mon Sep 17 00:00:00 2001 From: Luis Antonio Obis Aparicio <35803280+lobis@users.noreply.github.com> Date: Mon, 30 Sep 2024 13:20:47 +0200 Subject: [PATCH] GUI for N1471H (CAEN) (#63) * structure of the GUIs * Add new CAEN GUI * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add documentation * avoid adding class members dynamically * fix lambda * unused event * add cli arg parsing * lambda * rename * Add caenSimulator module * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Address https://github.com/lobis/hvps/pull/63#issuecomment-2313152396 * Fix previous commit: Address #63 (comment) * Customizable channel names * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * rename to follow python guidelines * typo * minor refactor * ruff * Fix for v0.1.1 * Erase 'clear & turn off all' button (not useful) * Improve alarm frame aesthetics * Improve apply_changes output message * do not use default mutable arguments * anotate method and address warnings * missing members on init * Use isinstance * Adjust number of decimals of vmon and imon * Use context manager to open and close the connection * Change vmon and imon entries to labels * Change main_frame to LabelFrame * Reduce unnecessary calls to the caen * Allow the ToolTip text to update dynamically * refactor CaenHVPSGUI initialization to accept a parent frame * add column with labels for vset * add 'Turn off multichannel' button * Bind key to set vset in the vset entries * make the status indicators circular (aesthetics) * Update README --------- Co-authored-by: Alvaro Ezquerro Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- gui/CAEN-N1471H/README.md | 54 +++ gui/CAEN-N1471H/caen_simulator.py | 163 ++++++++ gui/CAEN-N1471H/gui.py | 613 ++++++++++++++++++++++++++++++ gui/README.md | 2 + 4 files changed, 832 insertions(+) create mode 100644 gui/CAEN-N1471H/README.md create mode 100644 gui/CAEN-N1471H/caen_simulator.py create mode 100644 gui/CAEN-N1471H/gui.py create mode 100644 gui/README.md diff --git a/gui/CAEN-N1471H/README.md b/gui/CAEN-N1471H/README.md new file mode 100644 index 0000000..6d37409 --- /dev/null +++ b/gui/CAEN-N1471H/README.md @@ -0,0 +1,54 @@ +# CaenHVPSGUI Documentation + +## Overview +The `CaenHVPSGUI` is a graphical user interface (GUI) designed to control and monitor CAEN High Voltage Power Supply (HVPS) modules. The interface is built using Python's `tkinter` library, with threading to handle background tasks and maintain responsive interaction. The GUI supports various functionalities such as setting voltages, turning channels on or off, monitoring channel states, and handling alarms. + +The communication with the CAEN HVPS is done by the [hvps](https://github.com/lobis/hvps.git) python library. + +## Features +- **Real-time Monitoring**: Voltage (`vmon`), current (`imon`), and state indicators are updated in real-time. +- **Multi-Channel Support**: The GUI can handle modules with multiple channels, providing individual control and monitoring for each channel. +- **Alarm and Interlock Management**: Visual indicators and tooltips provide status information on the module alarms and interlock. +- **Threaded Background Processing**: Ensures the GUI remains responsive during long-running operations. +- **Tooltips**: Interactive tooltips provide additional information when hovering over various GUI elements. + +## How to Use +### Requirements +Make sure to have tkinter installed in your system (check [this](https://stackoverflow.com/a/74607246) if you don't) and the hvps python library (check [hvps installation guide](https://github.com/lobis/hvps?tab=readme-ov-file#installation-%EF%B8%8F)). + +Download the [gui.py](gui.py) script or clone the repository. +### Usage +At the directory where this [gui.py](gui.py) is found, run +``` bash +python3 gui.py --port /dev/ttyUSB0 +``` +Note that you may need to change the port. To show available ports run +``` bash +python -m hvps --ports +``` +#### Test mode +If you want to test the GUI without having the hardware available, copy the [caen_simulator.py](caen_simulator.py) module to the same directory where the [gui.py](gui.py) script is found and just run +``` bash +python3 gui.py --test +``` + +## Code Structure + +### Main Classes +- **ToolTip**: A class for creating and managing tooltips for GUI widgets. +- **CaenHVPSGUI**: The main class for creating and managing the GUI. + +### Key Methods +- GUI initializer and frames constructors: + - `create_gui()`: Initializes the GUI, setting up frames for alarms, channels, and other controls. + - `create_main_frame()`, `create_alarm_frame()`, `create_channels_frame()`: Methods for creating specific sections of the GUI. + - `open_channel_property_window()`: Opens a window with advanced settings for a specific channel. +- Action and user interaction handlers: + - `start_background_threads()`: Starts background threads for reading values and processing commands. + - `issue_command()`: Manages the queueing and execution of commands to the module. This is key to ensure the CAEN module does not receive multiple commands at the same time. Please, use this for any method that interacts with the CAEN module. + - `read_values()`: Continuously reads and updates the displayed values for each channel. + +### GUI Components +- **Alarm Frame**: Displays alarm and interlock indicators with buttons to clear signals. +- **Channels Frame**: Lists all available channels with options to set voltages, monitor values, and toggle states. +- **Multichannel Frame**: Provides batch operations on multiple channels. diff --git a/gui/CAEN-N1471H/caen_simulator.py b/gui/CAEN-N1471H/caen_simulator.py new file mode 100644 index 0000000..4df0599 --- /dev/null +++ b/gui/CAEN-N1471H/caen_simulator.py @@ -0,0 +1,163 @@ +import threading +import time +import random + + +# class that simulates the channel of the caen by giving a random value to its attributes vmon, imon and +class ChannelSimulator: + def __init__(self, trip_probability=0.02): + # private attributes (they do not exist in the real device) + self._trip_probability = trip_probability + self._vset = 100 + + # public attributes (they exist in the real device) + self.vset = 100 + self.iset = 0.6 # uA # functionality not implemented + self.vmon = self.vset + self.imon = self.vset / 10e3 # (uA) lets say there is a resistance of 10 MOhm + self.imdec = 3 # channel imon number of decimal digits (2 HR, 3 LR) + self.rup = 3 # V/s + self.rdw = 10 # V/s + self.pdwn = "RAMP" # 'KILL' or 'RAMP' + self.stat = { + "ON": True, + "RUP": False, + "RDW": False, + "OVC": False, + "OVV": False, + "UNV": False, + "MAXV": False, + "TRIP": False, + "OVP": False, + "OVT": False, + "DIS": False, + "KILL": False, + "ILK": False, + "NOCAL": False, + } + + def _randomize(self): + if self.stat["KILL"] or self.stat["DIS"]: + self.stat["ON"] = False + self.vmon = 0 + self.imon = 0 + return + + if self.stat["TRIP"] or self.stat["ILK"]: + self.stat["ON"] = False + if self.pdwn == "KILL": + self.stat["KILL"] = ( + True # not sure if it behaves like this in KILL mode + ) + self.vmon = 0 + self.imon = 0 + return + + self.vmon = random.gauss(self._vset, 1.0 / 3) # 0.3 V standard deviation + self.imon = random.gauss( + self.vmon / 10e3, self.vmon / 100e3 + ) # (uA) lets say there is a resistance of 10 MOhm and 10% standard deviation + + if not self.stat["ON"]: + self._vset -= self.rdw + self.imon = -self.imon * 10 + # self.stat["RDW"] = True # not sure + if self._vset <= 0: + self._vset = 0 + self.vmon = 0 + self.imon = 0 + return + + if not self.stat["TRIP"]: + self.stat["TRIP"] = random.random() < self._trip_probability + + # simulate ramp up and ramp down when the channel is ON + if self._vset < self.vset: + self.stat["RUP"] = True + self.stat["RDW"] = False + self._vset += self.rup + self.imon = self.imon * 10 + if self._vset > self.vset: + self._vset = self.vset + elif self._vset > self.vset: + self.stat["RDW"] = True + self.stat["RUP"] = False + self._vset -= self.rdw + self.imon = -self.imon * 10 + if self._vset < self.vset: + self._vset = self.vset + else: + self.stat["RUP"] = False + self.stat["RDW"] = False + + def turn_on(self): + self.stat["ON"] = True + self.stat["TRIP"] = False + self.stat["KILL"] = False + self.stat["ILK"] = False + + def turn_off(self): + self.stat["ON"] = False + self.stat["KILL"] = False + self.stat["TRIP"] = False + self.stat["ILK"] = False # not sure + + +class ModuleSimulator: + def __init__(self, n_channels, trip_probability=0.05): + self.name = "N1471H SIMULATOR" + self.number_of_channels = n_channels + self.channels = [ + ChannelSimulator( + 1 - (1 - trip_probability) ** (1.0 / self.number_of_channels) + ) + for i in range(self.number_of_channels) + ] + self.board_alarm_status = { + "CH0": False, + "CH1": False, + "CH2": False, + "CH3": False, + "PWFAIL": False, + "OVP": False, + "HVCKFAIL": False, + } + self.interlock_status = False + self.interlock_mode = "CLOSED" + + # Start the device reading thread + self.randomize_thread = threading.Thread( + target=self.__continuous_randomize, daemon=True + ).start() + + def clear_alarm_signal(self): + self.board_alarm_status = {k: False for k in self.board_alarm_status.keys()} + for ch in self.channels: + ch.stat["TRIP"] = False + ch.stat["ILK"] = False + + def _randomize(self): + # print(self.board_alarm_status, self.interlock_status) + # check for trips and set the alarm signal + for i, ch in enumerate(self.channels): + if ch.stat["TRIP"]: + self.board_alarm_status["CH" + str(i)] = True + # print(f"Channel {i} trip") + + # do the alarm-intlck connection and act the interlock + self.__connection_alarm_intlck() + if self.interlock_status: + for ch in self.channels: + ch.stat["ILK"] = True + + # randomize the channels + for ch in self.channels: + ch._randomize() + + def __continuous_randomize(self, wait_seconds=1): + while True: + self._randomize() + time.sleep(wait_seconds) + + def __connection_alarm_intlck(self): + self.interlock_status = any([v for k, v in self.board_alarm_status.items()]) diff --git a/gui/CAEN-N1471H/gui.py b/gui/CAEN-N1471H/gui.py new file mode 100644 index 0000000..4d253ee --- /dev/null +++ b/gui/CAEN-N1471H/gui.py @@ -0,0 +1,613 @@ +from __future__ import annotations + +import tkinter as tk +import threading +import queue +import time +import argparse + +import hvps + +CHANNEL_NAMES = {0: "mesh right", 1: "mesh left", 2: "gem top", 3: "gem bottom"} + + +class ToolTip: + def __init__(self, widget, text): + self.widget = widget + self.text = text + self.tooltip = None + self.label = None # To reference the Label for updating its text + + self.widget.bind("", lambda _: self.show_tooltip()) + self.widget.bind("", lambda _: self.hide_tooltip()) + + def show_tooltip(self): + if self.tooltip: # Tooltip is already being shown + self.label.config(text=self.text) # Just update the text + else: + x = self.widget.winfo_rootx() + 20 + y = self.widget.winfo_rooty() + 20 + self.tooltip = tk.Toplevel(self.widget) + self.tooltip.wm_overrideredirect(True) + self.tooltip.wm_geometry(f"+{x}+{y}") + + self.label = tk.Label( + self.tooltip, + text=self.text, + background="light goldenrod", + relief="solid", + borderwidth=1, + font=("Arial", 10), + ) + self.label.pack() + + def hide_tooltip(self): + if self.tooltip: + self.tooltip.destroy() + self.tooltip = None + self.label = None # Reset the reference to the label + + def change_text(self, text): + self.text = text + if self.tooltip: # If the tooltip is visible, update its text + self.label.config(text=self.text) + + +class CaenHVPSGUI: + def __init__(self, module, channel_names=None, parent_frame=None): + if channel_names is None: + channel_names = {} + + self.channel_vars = None + self.set_buttons = None + self.turn_buttons = None + self.state_tooltips = None + self.state_indicators = None + self.imon_labels = None + self.vmon_labels = None + self.vset_entries = None + self.vset_labels = None + + self.alarm_frame = None + self.set_multichannel_button = None + self.clear_alarm_button = None + self.interlock_indicator = None + self.interlock_tooltip = None + self.alarm_tooltip = None + self.alarm_indicator = None + self.multichannel_frame = None + self.channel_frame = None + self.main_frame = None + self.root = parent_frame + + self.m = module # Simulated module with 4 channels + self.channel_names = channel_names + for i in range(self.m.number_of_channels): + if i not in self.channel_names: + self.channel_names[i] = f"Channel {i}" # default name for the channel + self.command_queue = queue.Queue() + self.device_lock = threading.Lock() + + self.create_gui() + + def create_gui(self): + start_mainloop = False + if self.root is None: + self.root = tk.Tk() + self.root.title("Caen HVPS GUI") + start_mainloop = True + + self.main_frame = self.create_main_frame() + self.alarm_frame = self.create_alarm_frame(self.main_frame) + self.channel_frame = self.create_channels_frame(self.main_frame) + if self.m.number_of_channels > 1: + self.multichannel_frame = self.create_multichannel_frame(self.channel_frame) + + self.start_background_threads() + + if start_mainloop: + self.root.mainloop() + + def create_main_frame(self): + main_frame = tk.LabelFrame( + self.root, + text=f"Module {self.m.name}", + font=("", 16), + bg="lightgray", + labelanchor="n", + padx=10, + pady=10, + ) + main_frame.pack(fill="both", expand=True) + return main_frame + + def create_alarm_frame(self, frame): + alarm_frame = tk.Frame(frame, bg="gray", padx=20, pady=20) + alarm_frame.grid(row=1, column=0, padx=10, pady=10, sticky="N") + + tk.Label( + alarm_frame, text="Module", font=("Arial", 14, "bold"), bg="gray" + ).grid(row=0, column=0, columnspan=2) + + tk.Label( + alarm_frame, text="Alarm", font=("Arial", 12, "bold"), bg="gray", fg="black" + ).grid(row=1, column=0) + intlck_label = tk.Label( + alarm_frame, + text="Interlock", + font=("Arial", 12, "bold"), + bg="gray", + fg="black", + ) + intlck_label.grid(row=1, column=1) + ToolTip(intlck_label, f"Interlock mode: {self.m.interlock_mode}") + + self.alarm_indicator = tk.Canvas( + alarm_frame, width=30, height=30, bg="gray", highlightthickness=0 + ) + self.alarm_indicator.grid(row=2, column=0, padx=10, pady=10) + self.alarm_indicator.create_oval(2, 2, 28, 28, fill="gray") + self.alarm_tooltip = ToolTip(self.alarm_indicator, "Alarm signal") + + self.interlock_indicator = tk.Canvas( + alarm_frame, width=30, height=30, bg="gray", highlightthickness=0 + ) + self.interlock_indicator.grid(row=2, column=1, padx=10, pady=10) + self.interlock_indicator.create_oval(2, 2, 28, 28, fill="gray") + self.interlock_tooltip = ToolTip(self.interlock_indicator, "Interlock signal") + + self.clear_alarm_button = tk.Button( + alarm_frame, + text="Clear alarm signal", + font=("Arial", 10), + bg="navy", + fg="white", + command=lambda: self.issue_command(self.clear_alarm), + ) + self.clear_alarm_button.grid(row=3, column=0, columnspan=2, pady=20) + + return alarm_frame + + def create_channels_frame(self, frame): + channels_frame = tk.Frame(frame, bg="darkblue", padx=10, pady=10) + channels_frame.grid(row=1, column=1, padx=10, pady=10) + + tk.Label( + channels_frame, + text="Channels", + font=("Arial", 14, "bold"), + bg="darkblue", + fg="white", + ).grid(row=0, column=0, columnspan=7, pady=10) + tk.Label( + channels_frame, + text="state", + font=("Arial", 10, "bold"), + bg="darkblue", + fg="white", + ).grid(row=1, column=1) + tk.Label( + channels_frame, + text="Turn on/off", + font=("Arial", 10, "bold"), + bg="darkblue", + fg="white", + ).grid(row=1, column=2) + tk.Label( + channels_frame, + text="Set vset (V)", + font=("Arial", 10, "bold"), + bg="darkblue", + fg="white", + ).grid(row=1, column=3, columnspan=2) + tk.Label( + channels_frame, + text="vset (V)", + font=("Arial", 10, "bold"), + bg="darkblue", + fg="white", + ).grid(row=1, column=5) + tk.Label( + channels_frame, + text="vmon (V)", + font=("Arial", 10, "bold"), + bg="darkblue", + fg="white", + ).grid(row=1, column=6) + tk.Label( + channels_frame, + text="imon (uA)", + font=("Arial", 10, "bold"), + bg="darkblue", + fg="white", + ).grid(row=1, column=7) + + self.vset_entries = [] + self.vset_labels = [] + self.vmon_labels = [] + self.imon_labels = [] + self.state_indicators = [] + self.state_tooltips = [] + self.turn_buttons = [] + self.set_buttons = [] + for i in range(self.m.number_of_channels): + channel_button = tk.Button( + channels_frame, + text=f"{self.channel_names[i]}", + font=("Arial", 12, "bold"), + bg="darkblue", + fg="white", + borderwidth=0, + highlightthickness=0, + command=lambda x=i: self.issue_command( + self.open_channel_property_window, x + ), + ) + channel_button.grid(row=i + 2, column=0, padx=10, pady=5) + ToolTip(channel_button, f"Channel {i}: click for more setting options.") + + state_indicator = tk.Canvas( + channels_frame, width=30, height=30, bg="darkblue", highlightthickness=0 + ) + state_indicator.grid(row=i + 2, column=1, sticky="NSEW", padx=5, pady=5) + state_indicator.create_oval(2, 2, 28, 28, fill="black") + self.state_indicators.append(state_indicator) + self.state_tooltips.append(ToolTip(state_indicator, "State:")) + + turn_button = tk.Button( + channels_frame, + text="--------", + font=("Arial", 9), + bg="navy", + fg="white", + command=lambda x=i: self.issue_command(self.toggle_channel, x), + ) + turn_button.grid(row=i + 2, column=2, padx=35, pady=5) + self.turn_buttons.append(turn_button) + + set_button = tk.Button( + channels_frame, + text="Set", + font=("Arial", 9), + bg="navy", + fg="white", + command=lambda x=i: self.issue_command(self.set_vset, x), + ) + set_button.grid(row=i + 2, column=3, sticky="NSW", padx=0, pady=5) + self.set_buttons.append(set_button) + + vset_entry = tk.Entry(channels_frame, width=7, justify="center") + vset_entry.insert(0, str(self.m.channels[i].vset)) + vset_entry.grid(row=i + 2, column=4, sticky="NSE", padx=0, pady=5) + vset_entry.bind( + "", lambda event, x=i: self.issue_command(self.set_vset, x) + ) + self.vset_entries.append(vset_entry) + + vset_label = tk.Label(channels_frame, width=7, justify="center", text="-1") + vset_label.grid(row=i + 2, column=5, sticky="NS", padx=10, pady=5) + self.vset_labels.append(vset_label) + + vmon_label = tk.Label(channels_frame, width=7, justify="center", text="-1") + vmon_label.grid(row=i + 2, column=6, sticky="NS", padx=10, pady=5) + self.vmon_labels.append(vmon_label) + + imon_label = tk.Label(channels_frame, width=7, justify="center", text="-1") + imon_label.grid(row=i + 2, column=7, sticky="NS", padx=10, pady=5) + self.imon_labels.append(imon_label) + + return channels_frame + + def create_multichannel_frame(self, frame): + checkbox_frame = tk.Frame(frame, bg="darkblue") + checkbox_frame.grid( + row=self.m.number_of_channels + 2, column=1, columnspan=6, pady=10 + ) + + tk.Label( + checkbox_frame, + text="Multichannel control", + font=("Arial", 12, "bold"), + bg="darkblue", + fg="white", + ).grid(row=0, column=0, columnspan=2, pady=10) + + self.channel_vars = [] + nRows = 0 + for i in range(self.m.number_of_channels): + nRows += 1 + var = tk.IntVar() + self.channel_vars.append(var) + tk.Checkbutton( + checkbox_frame, + text=f" {self.channel_names[i]}", + variable=var, + font=("Arial", 10), + bg="darkblue", + fg="white", + selectcolor="gray", + borderwidth=0, + highlightthickness=0, + ).grid(row=i + 1, column=0, sticky="w", padx=20) + var.set(1) + + self.set_multichannel_button = tk.Button( + checkbox_frame, + text="Set multichannel", + font=("Arial", 10), + bg="navy", + fg="white", + command=lambda: self.issue_command(self.set_multichannel_vset_and_turn_on), + ) + self.set_multichannel_button.grid( + row=1, column=1, rowspan=int(nRows / 2), padx=20, pady=5 + ) + self.turn_off_multichannel_button = tk.Button( + checkbox_frame, + text="Turn off multichannel", + font=("Arial", 10), + bg="navy", + fg="white", + command=lambda: self.issue_command(self.turn_off_multichannel), + ) + self.turn_off_multichannel_button.grid( + row=int(nRows / 2) + 1, column=1, rowspan=int(nRows / 2), padx=20, pady=5 + ) + return frame + + def open_channel_property_window(self, channel_number): + def values_from_description(description_: str | dict) -> list[str]: + if isinstance(description_, str): + # e.g. 'VAL:XXXX.X Set VSET value' -> [] ; 'VAL:RAMP/KILL Set POWER DOWN mode value' -> ['RAMP', 'KILL'] + # for hvps version <= 0.1.0 + valid_values = description_.split("VAL:") + if len(valid_values) == 1: + return [] + valid_values = valid_values[1].split(" ")[0] + if "/" not in valid_values: + return [] + return valid_values.split("/") + elif isinstance(description_, dict): + # e.g. {'command' : 'PDWN', 'input_type': str, 'allowed_input_values': ['RAMP', 'KILL'], 'output_type': None, 'possible_output_values': []} + # e.g. {'command' : 'RUP', 'input_type': float, 'allowed_input_values': [], 'output_type': None, 'possible_output_values': []} + # for hvps version >= 0.1.1 + return description_["allowed_input_values"] + return [] + + # Crear la nueva ventana + new_window = tk.Toplevel(self.root) + new_window.title(f"{self.channel_names[channel_number]}") + new_window.configure(bg="darkblue") + + ch = self.m.channels[channel_number] + try: + set_properties = ( + hvps.commands.caen.channel._SET_CHANNEL_COMMANDS + ) # version >= 0.1.1 + except AttributeError: + set_properties = ( + hvps.commands.caen.channel._set_channel_commands + ) # version <= 0.1.0 + properties = ch.__dir__() + + entries = {} + for prop, description in set_properties.items(): + p = prop.lower() + if p not in properties or callable(getattr(ch, p)) or p == "vset": + continue + + label = tk.Label( + new_window, text=p, font=("Arial", 12), bg="blue", fg="white" + ) + label.grid(row=len(entries), column=0, padx=10, pady=5, sticky="e") + ToolTip( + label, + description["description"] + if isinstance(description, dict) + else description, + ) # hvps version < 0.1.1 is str, >= 0.1.1 is dict + + values = values_from_description(description) + if values: + selected_option = tk.StringVar(new_window) + selected_option.set(getattr(ch, p)) + + option_menu = tk.OptionMenu(new_window, selected_option, *values) + option_menu.config(font=("Arial", 12)) + option_menu.grid(row=len(entries), column=1, padx=10, pady=5) + entries[p] = option_menu + + else: + entry = tk.Entry( + new_window, font=("Arial", 12), width=10, justify="center" + ) + entry.grid(row=len(entries), column=1, padx=10, pady=5) + entry.insert(0, str(getattr(ch, p))) + entries[p] = entry + + # Botones "Cancel" y "Apply" + cancel_button = tk.Button( + new_window, + text="Cancel", + font=("Arial", 10), + bg="navy", + fg="white", + command=new_window.destroy, + ) + cancel_button.grid(row=len(properties), column=0, padx=10, pady=10, sticky="e") + + def apply_changes(): + print("Setting:") + for p, entry in entries.items(): + if entry.winfo_class() == "Menubutton": + value = entry.cget("text") + else: + value = entry.get() + try: + value = float(value) + except ValueError: + pass + setattr(ch, p, value) + print(f" {p}\t-> {value}") + new_window.destroy() + print() + + apply_button = tk.Button( + new_window, + text="Apply", + font=("Arial", 10), + bg="darkblue", + fg="white", + command=lambda: self.issue_command(apply_changes), + ) + apply_button.grid(row=len(properties), column=1, padx=10, pady=10, sticky="w") + + def start_background_threads(self): + threading.Thread(target=self.read_loop, daemon=True).start() + threading.Thread(target=self.process_commands, daemon=True).start() + + def process_commands(self): + while True: + func, args, kwargs = self.command_queue.get() + with self.device_lock: + func(*args, **kwargs) + self.command_queue.task_done() + if self.root.cget("cursor") == "watch" and func.__name__ != "read_values": + self.root.config(cursor="") + + def issue_command(self, func, *args, **kwargs): + # do not stack read_values commands (critical if reading values is slow) + if ( + func.__name__ == "read_values" + and (func, args, kwargs) in self.command_queue.queue + ): + return + # print('\n'), [print(i) for i in self.command_queue.queue] # debug + self.command_queue.put((func, args, kwargs)) + if ( + func.__name__ != "read_values" + ): # because it is constantly reading values in the background + self.root.config(cursor="watch") + self.root.update() + + def set_vset(self, channel_number): + try: + vset_value = float(self.vset_entries[channel_number].get()) + except ValueError: + self.vset_entries[channel_number].delete(0, tk.END) + self.vset_entries[channel_number].insert( + 0, str(self.m.channels[channel_number].vset) + ) + print("ValueError: Set voltage value must be a number") + return + self.m.channels[channel_number].vset = vset_value + + def set_multichannel_vset_and_turn_on(self): + for i, entry in enumerate(self.vset_entries): + if self.channel_vars[i].get(): + self.set_vset(i) + self.m.channels[i].turn_on() + entry.delete(0, tk.END) + entry.insert(0, str(self.m.channels[i].vset)) + + def turn_off_multichannel(self): + for i, chvar in enumerate(self.channel_vars): + if chvar.get(): + self.m.channels[i].turn_off() + + def clear_alarm(self): + self.m.clear_alarm_signal() + + def toggle_channel(self, channel_number): + ch = self.m.channels[channel_number] + if ch.stat["ON"]: + ch.turn_off() + else: + self.set_vset(channel_number) + ch.turn_on() + + def set_vset_and_turn_on(self, channel_number): + entry = self.vset_entries[channel_number] + self.m.channels[channel_number].vset = float(entry.get()) + self.m.channels[channel_number].turn_on() + entry.delete(0, tk.END) + entry.insert(0, str(self.m.channels[channel_number].vset)) + + def read_loop(self): + while True: + self.issue_command(self.read_values) + time.sleep(1) + + def read_values(self): + for i, ch in enumerate(self.m.channels): + self.vset_labels[i].config(text=f"{ch.vset:.1f}") + self.vmon_labels[i].config(text=f"{ch.vmon:.1f}") + self.imon_labels[i].config(text=f"{ch.imon:.3f}") + self.update_state_indicator(i, ch) + self.update_alarm_indicators() + + def update_state_indicator(self, channel_number, channel): + # Update the state indicator + stat = channel.stat.copy() + if stat["TRIP"]: + state_indicator_color = "red" + state_tooltip_text = "TRIP" + elif stat["DIS"]: + state_indicator_color = "black" + state_tooltip_text = "DISABLED" + elif stat["KILL"]: + state_indicator_color = "orange" + state_tooltip_text = "KILL" + elif stat["ILK"]: + state_indicator_color = "yellow" + state_tooltip_text = "INTERLOCK" + else: + if stat["ON"]: + state_indicator_color = "green2" + state_tooltip_text = "ON" + else: + state_indicator_color = "dark green" + state_tooltip_text = "OFF" + if stat["RUP"]: + state_tooltip_text += " (RAMP UP)" + if stat["RDW"]: + state_tooltip_text += " (RAMP DOWN)" + self.state_indicators[channel_number].itemconfig(1, fill=state_indicator_color) + self.state_tooltips[channel_number].change_text(f"State: {state_tooltip_text}") + + self.turn_buttons[channel_number].configure( + text="TURN OFF" if stat["ON"] else "TURN ON" + ) + + def update_alarm_indicators(self): + bas = self.m.board_alarm_status.copy() + ilk = self.m.interlock_status + self.alarm_indicator.itemconfig( + 1, fill="red" if any([v for k, v in bas.items()]) else "green" + ) + self.alarm_tooltip.change_text( + f"Alarm signal: {[k for k, v in bas.items() if v]}" + ) + self.interlock_indicator.itemconfig(1, fill="red" if ilk else "green") + self.interlock_tooltip.change_text(f"Interlock signal: {ilk}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--test", action="store_true", help="Enable test mode") + parser.add_argument("--port", type=str, help="Select port", default="/dev/ttyUSB0") + + args = parser.parse_args() + + if not args.test: + with hvps.Caen(port=args.port) as caen: + print("port:", caen.port) + print("baudrate:", caen.baudrate) + m = caen.module(0) + CaenHVPSGUI(module=m, channel_names=CHANNEL_NAMES) + + else: + from caen_simulator import * # noqa: F403 + + m = ModuleSimulator(4) # noqa: F405 + CaenHVPSGUI(module=m, channel_names=CHANNEL_NAMES) diff --git a/gui/README.md b/gui/README.md new file mode 100644 index 0000000..d3fa2aa --- /dev/null +++ b/gui/README.md @@ -0,0 +1,2 @@ + +Here are some examples of GUIs to control high voltage power supplies using the `HVPS` library.