diff --git a/src/navigate/config/config.py b/src/navigate/config/config.py index 8ddc7653d..fdbe70047 100644 --- a/src/navigate/config/config.py +++ b/src/navigate/config/config.py @@ -44,7 +44,7 @@ import yaml # Local Imports - +from navigate.tools.common_functions import build_ref_name def get_navigate_path(): """Establish a program home directory in AppData/Local/.navigate for Windows @@ -764,10 +764,8 @@ def verify_waveform_constants(manager, configuration): waveform_dict[microscope_name][zoom], laser, { - "amplitude": config_dict["remote_focus_device"][ - "amplitude" - ], - "offset": config_dict["remote_focus_device"]["offset"], + "amplitude": 0, + "offset": 0, # "percent_smoothing": "0", # "delay": config_dict["remote_focus_device"][ # "delay" @@ -864,9 +862,9 @@ def verify_waveform_constants(manager, configuration): waveform_dict[microscope_name], zoom, { - "amplitude": "0.11", - "offset": config_dict["galvo"][i]["offset"], - "frequency": config_dict["galvo"][i]["frequency"], + "amplitude": "0", + "offset": 0, + "frequency": 10, }, ) else: @@ -896,12 +894,8 @@ def verify_waveform_constants(manager, configuration): other_constants_dict = { "remote_focus_settle_duration": "0", "percent_smoothing": "0", - "remote_focus_delay": config_dict["remote_focus_device"][ - "delay" - ], - "remote_focus_ramp_falling": config_dict["remote_focus_device"][ - "ramp_falling" - ] + "remote_focus_delay": "0", + "remote_focus_ramp_falling": "5" } if ( "other_constants" not in waveform_dict.keys() @@ -924,7 +918,16 @@ def verify_configuration(manager, configuration): Supports old version of configurations. """ + channel_count = 5 + # generate hardware header section device_config = configuration["configuration"]["microscopes"] + hardware_dict = {} + ref_list = { + "camera": [], + "stage": [], + "zoom": None, + "mirror": None, + } for microscope_name in device_config.keys(): # camera # delay_percent -> delay @@ -938,3 +941,54 @@ def verify_configuration(manager, configuration): remote_focus_config["ramp_falling"] = remote_focus_config.get("ramp_falling_percent", 5) if "delay" not in remote_focus_config.keys(): remote_focus_config["delay"] = remote_focus_config.get("delay_percent", 0) + + # daq + daq_type = device_config[microscope_name]["daq"]["hardware"]["type"] + if not daq_type.lower().startswith("synthetic"): + hardware_dict["daq"] = {"type": daq_type} + + # camera + if "camera" not in hardware_dict: + hardware_dict["camera"] = [] + camera_idx = build_ref_name("-", camera_config["hardware"]["type"], camera_config["hardware"]["serial_number"]) + if camera_idx not in ref_list["camera"]: + ref_list["camera"].append(camera_idx) + hardware_dict["camera"].append(camera_config["hardware"]) + + try: + channel_count = max(channel_count, camera_config.get("count", 5)) + except TypeError: + channel_count = 5 + + # zoom (one zoom) + if "zoom" not in hardware_dict: + zoom_config = device_config[microscope_name]["zoom"]["hardware"] + # zoom_idx = build_ref_name("-", zoom_config["type"], zoom_config["servo_id"]) + hardware_dict["zoom"] = zoom_config + + # filter wheel + if "filter_wheel" not in hardware_dict: + filter_wheel_config = device_config[microscope_name]["filter_wheel"]["hardware"] + hardware_dict["filter_wheel"] = filter_wheel_config + # stage + if "stage" not in hardware_dict: + hardware_dict["stage"] = [] + stages = device_config[microscope_name]["stage"]["hardware"] + if type(stages) != ListProxy: + stages = [stages] + for i, stage in enumerate(stages): + stage_idx = build_ref_name("-", stage["type"], stage["serial_number"]) + if stage_idx not in ref_list["stage"]: + hardware_dict["stage"].append(stage) + + # mirror + if "mirror" in device_config[microscope_name].keys() and "mirror" not in hardware_dict: + hardware_dict["mirror"] = device_config[microscope_name]["mirror"]["hardware"] + + if "daq" not in hardware_dict: + hardware_dict["daq"] = { + "type": "synthetic" + } + update_config_dict(manager, configuration["configuration"], "hardware", hardware_dict) + + update_config_dict(manager, configuration["configuration"], "gui", {"channels": {"count": channel_count}}) diff --git a/src/navigate/config/configuration_database.py b/src/navigate/config/configuration_database.py new file mode 100644 index 000000000..2c69fd408 --- /dev/null +++ b/src/navigate/config/configuration_database.py @@ -0,0 +1,243 @@ +camera_device_types = { + "Hamamatsu ORCA Lightning": "HamamatsuOrcaLightning", + "Hamamatsu ORCA Fire": "HamamatsuOrcaFire", + "Hamamatsu Flash 4.0": "HamamatsuOrca", + "Photometrics Iris 15B": "Photometrics", + "Virtual Device": "synthetic" +} + +camera_hardware_widgets = { + "hardware/type": ["Device Type", "Combobox", "string", camera_device_types, None], + "hardware/serial_number": ["Serial Number", "Input", "string", None, 'Example: "302352"'], + "hardware/camera_connection": ["Camera Connection", "Input", "string", None, "*Photometrics Iris 15B only"], + "defect_correct_mode": ["Defect Correct Mode", "Combobox", "string", {"On": 2.0, "Off": 1.0}, None], + "delay": ["Delay (ms)", "Spinbox", "float", None, None], + "flip_x": ["Flip X", "Checkbutton", "bool", None, None], + "flip_y": ["Flip Y", "Checkbutton", "bool", None, None], + "count": ["Microscope Channel Count", "Spinbox", "int", {"from": 5, "to": 10, "step": 1}, None] +} + +filter_wheel_device_types = { + "Sutter Instruments": "SutterFilterWheel", + "Applied Scientific Instrumentation": "ASI", + "Virtual Device": "synthetic", +} + +filter_wheel_widgets = { + "filter_name": ["Filter Name", "Input", "string", None, "Example: Empty-Alignment"], + "filter_value": ["Filter Value", "Input", "string", None, "Example: 0"], + "button_1": ["Delete", "Button", {"delete": True}], + "frame_config": {"ref": "available_filters", "format": "item(filter_name,filter_value),", "direction": "horizon"} +} + +filter_wheel_hardware_widgets = { + "hardware/type": ["Device Type", "Combobox", "string", filter_wheel_device_types, None], + "hardware/wheel_number": ["Number of Wheels", "Spinbox", "int", None, "Example: 1"], + "hardware/port": ["Serial Port", "Input", "string", None, "Example: COM1"], + "hardware/baudrate": ["Baudrate", "Input", "int", None, "Example: 9200"], + "filter_wheel_delay": ["Filter Wheel Delay (s)", "Input", "float", None, "Example: 0.03"], + "button_1": ["Add Available Filters", "Button", {"widgets":filter_wheel_widgets, "ref": "available_filters", "direction": "horizon"}] +} + +daq_device_types = { + "National Instruments": "NI", +} + +daq_hardware_widgets = { + "hardware/type": ["Device Type", "Combobox", "string", daq_device_types, None], + "sample_rate": ["Sample Rate", "Input", "int", None, "Example: 9600"], + "master_trigger_out_line": ["Master Trigger Out", "Input", "string", None, "Example: PXI6259/port0/line1"], + "camera_trigger_out_line": ["Camera Trigger Out", "Input", "string", None, "Example: /PXI6259/ctr0"], + "trigger_source": ["Trigger Source", "Input", "string", None, "Example: /PXI6259/PFI0"], + "laser_port_switcher": ["Laser Switcher Port", "Input", "string", None, "Example: PXI6733/port0/line0"], + "laser_switch_state": ["Laser Switch On State", "Combobox", "bool", [True, False], None], +} + +shutter_device_types = { + "Analog/Digital Device": "NI", + "Virtual Device": "synthetic", +} + +shutter_hardware_widgets = { + "type": ["Device Type", "Combobox", "string", shutter_device_types, None], + "channel": ["NI Channel", "Input", "string", None, "Example: PXI6259/port0/line0"], + "min": ["Minimum Voltage", "Spinbox", "float", None, "Example: 0"], + "max": ["Maximum Voltage", "Spinbox", "float", None, "Example: 5"], + "frame_config": {"ref": "hardware"} +} + +stage_device_types = { + "Applied Scientific Instrumentation": "ASI", + "Analog/Digital Device": "GalvoNIStage", + "Mad City Labs": "MCL", + "Physik Instrumente": "PI", + "Sutter Instruments": "MP285", + "ThorLabs KCube Inertial Device": "Thorlabs", + "Virtual Device": "synthetic", +} + +stage_hardware_widgets = { + "type": ["Device Type", "Combobox", "string", stage_device_types, None], + "serial_number": ["Serial Number", "Input", "string", None, None], + "axes": ["Axes", "Input", "string", None, "Example: [x, y, z]"], + "axes_mapping": ["Axes Mapping", "Input", "string", None, "Example: [X, M, Y]"], + "volts_per_micron": ["Volts Per Micron", "Spinbox", "float", {"from": 0, "to": 100, "step":0.1}, "*Analog/Digital Device only"], + "min": ["Minimum Volts", "Spinbox", "float", {"from": 0, "to": 5, "step": 0.1}, "*Analog/Digital Device only",], + "max": ["Maximum Volts", "Spinbox", "float", {"from": 1, "to": 100, "step": 0.1}, "*Analog/Digital Device only",], + "controllername": ["Controller Name", "Input", "string", None, "*Physik Instrumente only. Example: 'C-884'"], + "stages": ["PI Stages", "Input", "string", None, "*Physik Instrumente only. Example: L-509.20DG10 L-509.40DG10"], + "refmode": ["REF Modes", "Input", "string", None, "*Physik Instrumente only. Example: FRF FRF"], + "port": ["Serial Port", "Input", "string", None, "Example: COM1"], + "baudrate": ["Baudrate", "Input", "int", None, "Example: 9200"], + "button_2": ["Delete", "Button", {"delete": True}], + "frame_config": {"collapsible": True, "title": "Stage", "ref": "hardware", "format": "list-dict"} +} + +stage_top_widgets = { + "button_1": ["Add New Stage Device", "Button", {"widgets": stage_hardware_widgets, "ref": "hardware", "parent": "hardware"}], +} + +stage_constants_widgets = { + "joystick_axes": ["Joystick Axes", "Input", "string", None, "Example: [x, y, z]"], + "x_min": ["Min X", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}, None], + "x_max": ["Max X", "Spinbox", "float", {"from": 0, "to": 10000, "step": 1000}, None], + "y_min": ["Min Y", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}, None], + "y_max": ["Max Y", "Spinbox", "float", {"from": 0, "to": 10000, "step": 1000}, None], + "z_min": ["Min Z", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}, None], + "z_max": ["Max Z", "Spinbox", "float", {"from": 0, "to": 10000, "step": 1000}, None], + "theta_min": ["Min Theta", "Spinbox", "float", {"from": 0, "to": 360, "step": 1000}, None], + "theta_max": ["Max Theta", "Spinbox", "float", {"from": 0, "to": 360, "step": 1000}, None], + "f_min": ["Min Focus", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}, None], + "f_max": ["Max Focus", "Spinbox", "float", {"from": 0, "to": 10000, "step": 1000}, None], + "x_offset": ["Offset of X", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}, "Example: 0"], + "y_offset": ["Offset of Y", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}, "Example: 0"], + "z_offset": ["Offset of Z", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}, "Example: 0"], + "theta_offset": ["Offset of Theta", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}, "Example: 0"], + "f_offset": ["Offset of Focus", "Spinbox", "float", {"from": -100000, "to": 10000, "step": 1000}, "Example: 0"], + "frame_config": {"collapsible": True, "title": "Stage Constants"} +} + +remote_focus_device_types = { + "Equipment Solutions": "EquipmentSolutions", + "Analog Device": "NI", + "Virtual Device": "synthetic" +} + +remote_focus_hardware_widgets = { + "type": ["Device Type", "Combobox", "string", remote_focus_device_types, None], + "channel": ["DAQ Channel", "Input", "string", None, "Example: PXI6259/ao3"], + "min": ["Minimum Voltage", "Spinbox", "float", {"from": -10, "to": 10, "step": 1}, None], + "max": ["Maximum Voltage", "Spinbox", "float", {"from": 0, "to": 10, "step": 1}, None], + "comport": ["Serial Port", "Input", "string", None, "*Equipment Solutions only"], + "baudrate": ["Baudrate", "Input", "int", None, "*Equipment Solutions only. Example: 9200"], + "frame_config": {"ref": "hardware"} +} + +galvo_device_types = { + "Analog Device": "NI", + "Virtual Device": "synthetic" +} + +waveform_types = { + "Sine": "sine", + "Sawtooth": "sawtooth", + "Square": "square", +} + +galvo_hardware_widgets = { + "hardware/type": ["Device Type", "Combobox", "string", galvo_device_types, None], + "hardware/channel": ["DAQ Channel", "Input", "string", None, "Example: PXI6259/ao1"], + "hardware/min": ["Minimum Voltage", "Spinbox", "float", {"from": -10, "to": 10, "step": 0.1}, None], + "hardware/max": ["Maximum Voltage", "Spinbox", "float", {"from": 0, "to": 10, "step": 0.1}, None], + "waveform": ["Waveform", "Combobox", "string", waveform_types, None], + "phase": ["Phase", "Input", "string", None, "Example: 1.57"], + "button_1": ["Delete", "Button", {"delete": True}], + "frame_config": {"collapsible": True, "title": "Galvo Device", "ref": "None", "format": "list-dict"} +} + +galvo_top_widgets = { + "button_1": ["Add New Device", "Button", {"widgets": galvo_hardware_widgets, "parent": "hardware"}], +} + +zoom_device_types = { + "Dynamixel": "DynamixelZoom", + "Virtual Device": "synthetic" +} + +zoom_position_widgets = { + "zoom_value": ["Zoom Value", "Input", "string", None, "Example: 16x"], + "position": ["Position", "Input", "float", None, "Example: 1000"], + "pixel_size": ["Pixel Size (um)", "Input", "float", None, "Example: 0.5"], + "button_1": ["Delete", "Button", {"delete": True}], + "frame_config": {"ref": "position;pixel_size", "format": "item(zoom_value, position);item(zoom_value, pixel_size)", "direction": "horizon"} +} + +zoom_hardware_widgets = { + "type": ["Device Type", "Combobox", "string", zoom_device_types, None], + "servo_id": ["Servo ID", "Input", "string", None, "Example: 1"], + "port": ["Serial Port", "Input", "string", None, "Example: COM1"], + "baudrate": ["Baudrate", "Input", "int", None, "Example: 9600"], + "button_1": ["Add Zoom Value", "Button", {"widgets":zoom_position_widgets, "ref": "position;pixel_size", "direction": "horizon"}], + "frame_config": {"ref": "hardware"} +} + +mirror_device_types = { + "Imagine Optics": "ImagineOpticsMirror", + "Virtual Device": "SyntheticMirror" +} + +mirror_hardware_widgets = { + "type": ["Device Type", "Combobox", "string", mirror_device_types, None], + "frame_config": {"ref": "hardware"} +} + +laser_device_types = { + "Analog Device": "NI", + "Virtual Device": "synthetic" +} + +laser_hardware_widgets = { + "wavelength": ["Wavelength", "Input", "int", None, None, "Example: 488"], + "onoff": ["On/Off Setting", "Label", None, None, None], + "onoff/hardware/type": ["Type", "Combobox", "string", laser_device_types, None], + "onoff/hardware/channel": ["DAQ Channel", "Input", "string", None, "Example: PXI6733/port0/line2"], + "onoff/hardware/min": ["Minimum Voltage", "Spinbox", "float", {"from": 0, "to": 100, "step": 1}, None], + "onoff/hardware/max": ["Maximum Voltage", "Spinbox", "float", {"from": 0, "to": 100, "step": 1}, None], + "power": ["Power Setting", "Label", None, None, None], + "power/hardware/type": ["Type", "Combobox", "string", laser_device_types, None], + "power/hardware/channel": ["DAQ Channel", "Input", "string", None, "Example: PXI6733/ao0"], + "power/hardware/min": ["Minimum Voltage", "Spinbox", "float", {"from": 0, "to": 100, "step": 1}, None], + "power/hardware/max": ["Maximum Voltage", "Spinbox", "float", {"from": 0, "to": 100, "step": 1}, None], + "button_1": ["Delete", "Button", {"delete": True}], + "frame_config": {"collapsible": True, "title": "Wavelength", "format": "list-dict", "ref": "None"} +} + +laser_top_widgets = { + "button_1": ["Add Wavelength", "Button", {"widgets": laser_hardware_widgets, "parent": "hardware"}], +} + +hardwares_dict = { + "Camera": camera_hardware_widgets, + "Data Acquisition Card": daq_hardware_widgets, + "Filter Wheel": (None, filter_wheel_hardware_widgets, filter_wheel_widgets), + "Galvo": (galvo_top_widgets, galvo_hardware_widgets, None), + "Lasers": (laser_top_widgets, laser_hardware_widgets, None), + "Remote Focus Devices": remote_focus_hardware_widgets, + "Adaptive Optics": mirror_hardware_widgets, + "Shutters": shutter_hardware_widgets, + "Stages": (stage_top_widgets, stage_hardware_widgets, stage_constants_widgets), + "Zoom Device": (None, zoom_hardware_widgets, zoom_position_widgets) +} + +hardwares_config_name_dict = { + "Camera": "camera", + "Data Acquisition Card": "daq", + "Filter Wheel": "filter_wheel", + "Galvo": "galvo", + "Lasers": "lasers", + "Remote Focus Devices": "remote_focus_device", + "Adaptive Optics": "mirror", + "Shutters": "shutter", + "Stages": "stage", + "Zoom Device": "zoom", +} \ No newline at end of file diff --git a/src/navigate/controller/configurator.py b/src/navigate/controller/configurator.py new file mode 100644 index 000000000..73ce28b7b --- /dev/null +++ b/src/navigate/controller/configurator.py @@ -0,0 +1,441 @@ +# Copyright (c) 2021-2022 The University of Texas Southwestern Medical Center. +# All rights reserved. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted for academic and research use only +# (subject to the limitations in the disclaimer below) +# provided that the following conditions are met: + +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. + +# * Neither the name of the copyright holders nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. + +# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# Standard Library Imports +import tkinter as tk +from time import sleep +from tkinter import filedialog, messagebox + +# Third Party Imports + +# Local Imports +from navigate.view.configurator_application_window import ConfigurationAssistantWindow +from navigate.view.configurator_application_window import ( + MicroscopeTab, + MicroscopeWindow, +) +from navigate.config.configuration_database import ( + hardwares_dict, + hardwares_config_name_dict, +) +from navigate.tools.file_functions import load_yaml_file + +# Logger Setup +import logging + +p = __name__.split(".")[1] +logger = logging.getLogger(p) + + +class Configurator: + """Navigate Configurator""" + + def __init__(self, root, splash_screen): + """Initiates the configurator application window. + + Parameters + ---------- + root : tk.Tk + The main window of the application + splash_screen : SplashScreen + The splash screen of the application + """ + self.root = root + + # Show the splash screen for 1 second and then destroy it. + sleep(1) + splash_screen.destroy() + self.root.deiconify() + self.view = ConfigurationAssistantWindow(root) + self.view.microscope_window = MicroscopeWindow( + self.view.microscope_frame, self.view.root + ) + + self.view.top_window.add_button.config(command=self.add_microscope) + self.view.top_window.new_button.config(command=self.new_configuration) + self.view.top_window.load_button.config(command=self.load_configuration) + self.view.top_window.save_button.config(command=self.save) + self.view.top_window.cancel_button.config(command=self.on_cancel) + self.create_config_window(0) + self.microscope_id = 1 + + print( + "WARNING: The Configuration Assistant is not fully implemented. " + "Users are still required to manually configure their system." + ) + + def on_cancel(self): + """Closes the window and exits the program""" + self.root.destroy() + exit() + + def add_microscope(self): + """Add a new microscope tab""" + self.create_config_window(self.microscope_id) + self.microscope_id += 1 + + def delete_microscopes(self): + """Delete all microscopes""" + # delete microscopes + for index in range(self.view.microscope_window.index("end")): + tab_id = self.view.microscope_window.tabs()[index] + self.view.microscope_window.forget(tab_id) + self.view.microscope_window.tab_list = [] + self.microscope_id = 0 + + def new_configuration(self): + """Create new configurations""" + self.delete_microscopes() + self.create_config_window(self.microscope_id) + + def save(self): + """Save configuration file""" + + def set_value(temp_dict, key_list, value): + """Set value + + Parameters + ---------- + temp_dict: dict + Target dictionary + key_list: list + keyword list + value: any + value of the item + """ + if type(key_list) is list: + for i in range(len(key_list) - 1): + k = key_list[i] + temp_dict[k] = temp_dict.get(k, {}) + temp_dict = temp_dict[k] + temp_dict[key_list[-1]] = value + + filename = filedialog.asksaveasfilename( + defaultextension=".yml", filetypes=[("Yaml file", "*.yml *.yaml")] + ) + if not filename: + return + # warning_info + warning_info = {} + config_dict = {} + for tab_index in self.view.microscope_window.tabs(): + microscope_name = self.view.microscope_window.tab(tab_index, "text") + microscope_tab = self.view.microscope_window.nametowidget(tab_index) + microscope_dict = {} + config_dict[microscope_name] = microscope_dict + for hardware_tab_index in microscope_tab.tabs(): + hardware_name = microscope_tab.tab(hardware_tab_index, "text") + hardware_tab = microscope_tab.nametowidget(hardware_tab_index) + hardware_dict = {} + microscope_dict[ + hardwares_config_name_dict.get(hardware_name, hardware_name) + ] = hardware_dict + for variable_list in hardware_tab.variables_list: + if variable_list is None: + continue + variables, value_dict, ref, format = variable_list + if format is None: + format = "" + temp_dict = hardware_dict + if ref is not None: + if format.startswith("list"): + hardware_dict[ref] = hardware_dict.get(ref, []) + temp_dict = {} + hardware_dict[ref].append(temp_dict) + elif format.startswith("item"): + format_list = format.split(";") + ref_list = ref.split(";") + for i, format in enumerate(format_list): + ref = ref_list[i] + hardware_dict[ref] = hardware_dict.get(ref, {}) + temp_dict = hardware_dict[ref] + k_idx = format[ + format.index("(") + 1 : format.index(",") + ].strip() + v_idx = format[ + format.index(",") + 1 : format.index(")") + ].strip() + k = variables[k_idx].get() + if k.strip() == "": + warning_info[hardware_name] = True + print( + f"Notice: {hardware_name} has an empty value {ref}! Please double check if it's okay!" + ) + + if k_idx in value_dict: + k = value_dict[k_idx][v] + v = variables[v_idx].get() + if v_idx in value_dict: + v = value_dict[v_idx][v] + temp_dict[k] = v + continue + else: + temp_dict = {} + hardware_dict[ref] = hardware_dict.get("ref", temp_dict) + for k, var in variables.items(): + try: + if k in value_dict: + v = value_dict[k][var.get()] + else: + v = var.get() + except tk._tkinter.TclError: + v = "" + print( + f"Notice: {hardware_name} has an empty value {k}! Please double check!" + ) + warning_info[hardware_name] = True + set_value(temp_dict, k.split("/"), v) + + self.write_to_yaml(config_dict, filename) + # display warning + if warning_info: + messagebox.showwarning( + title="Configuration", + message=f"There are empty value(s) with {', '.join(warning_info.keys())}. Please double check!" + ) + + def write_to_yaml(self, config, filename): + """write yaml file + + Parameters + ---------- + config: dict + configuration dictionary + filename: str + yaml file name + """ + + def write_func(prefix, config_dict, f): + for k in config_dict: + if type(config_dict[k]) == dict: + f.write(f"{prefix}{k}:\n") + write_func(prefix + " " * 2, config_dict[k], f) + elif type(config_dict[k]) == list: + list_prefix = " " + if k != "None": + f.write(f"{prefix}{k}:\n") + list_prefix = " " * 2 + for list_item in config_dict[k]: + f.write(f"{prefix}{list_prefix}-\n") + write_func(prefix + list_prefix * 2, list_item, f) + else: + f.write(f"{prefix}{k}: {config_dict[k]}\n") + + with open(filename, "w") as f: + f.write("microscopes:\n") + write_func(" ", config, f) + + def create_config_window(self, id): + """Creates the configuration window tabs.""" + + tab_name = "Microscope-" + str(id) + microscope_tab = MicroscopeTab( + self.view.microscope_window, + root=self.view.root, + ) + self.view.microscope_window.tab_list.append(tab_name) + for hardware_type, widgets in hardwares_dict.items(): + if not widgets: + continue + if type(widgets) == dict: + microscope_tab.create_hardware_tab(hardware_type, widgets) + else: + microscope_tab.create_hardware_tab( + hardware_type, + hardware_widgets=widgets[1], + widgets=widgets[2], + top_widgets=widgets[0], + ) + + # Adding tabs to self notebook + self.view.microscope_window.add( + microscope_tab, + text=tab_name, + sticky=tk.NSEW, + ) + + def load_configuration(self): + """Load configuration""" + + def get_widget_value(name, value_dict): + """Get the value from a dict""" + value = value_dict + for key in name.split("/"): + if key.strip() == "": + return value + value = value.get(key, None) + if value is None: + return None + return value + + def get_widgets_value(widgets, value_dict): + """Get all key-value from valude_dict, keys are from widgets""" + temp = {} + for key in widgets: + if key == "frame_config": + continue + if widgets[key][1] in ["Button", "Label"]: + continue + value = get_widget_value(key, value_dict) + # widgets[key][3] is the value mapping dict + if widgets[key][1] != "Spinbox" and widgets[key][3]: + reverse_value_dict = dict( + map(lambda v: (v[1], v[0]), widgets[key][3].items()) + ) + temp[key] = reverse_value_dict[value] + else: + temp[key] = value + return temp + + def build_widgets_value(widgets, value_dict): + """According to valude_dict build values for widgets""" + if widgets is None or value_dict is None: + return [None] + result = [] + ref = "" + format = "" + if "frame_config" in widgets: + ref = widgets["frame_config"].get("ref", "") + format = widgets["frame_config"].get("format", "") + if format.startswith("list"): + if ref != "" and ref.lower() != "none": + value_dict = get_widget_value(ref, value_dict) + if type(value_dict) is not list: + return [None] + for i in range(len(value_dict)): + result.append(get_widgets_value(widgets, value_dict[i])) + elif format.startswith("item"): + format_list = format.split(";") + ref_list = ref.split(";") + for i, format_item in enumerate(format_list): + k_idx = format_item[ + format_item.index("(") + 1 : format_item.index(",") + ].strip() + v_idx = format_item[ + format_item.index(",") + 1 : format_item.index(")") + ].strip() + temp_widget_values = get_widget_value(ref_list[i], value_dict) + for j, k in enumerate(temp_widget_values.keys()): + if len(result) < j + 1: + result.append({k_idx: k, v_idx: temp_widget_values[k]}) + else: + result[j][k_idx] = k + result[j][v_idx] = temp_widget_values[k] + else: + if ref != "" and ref.lower() != "none": + value_dict = get_widget_value(ref, value_dict) + result.append(get_widgets_value(widgets, value_dict)) + + return result + # ask file name + file_name = filedialog.askopenfilename( + defaultextension=".yml", filetypes=[("Yaml file", "*.yml *.yaml")] + ) + if not file_name: + return + + # read configuration.yaml + config_dict = load_yaml_file(file_name) + if config_dict is None or "microscopes" not in config_dict: + messagebox.showerror( + title="Configuration", + message="It's not a valid configuration.yaml file!" + ) + return + + self.delete_microscopes() + + for i, microscope_name in enumerate(config_dict["microscopes"].keys()): + microscope_tab = MicroscopeTab( + self.view.microscope_window, + root=self.view.root, + ) + self.view.microscope_window.add( + microscope_tab, + text=microscope_name, + sticky=tk.NSEW, + ) + self.view.microscope_window.tab_list.append(microscope_name) + + for hardware_type, widgets in hardwares_dict.items(): + hardware_ref_name = hardwares_config_name_dict[hardware_type] + # build dictionary values for widgets + if type(widgets) == dict: + try: + widgets_value = build_widgets_value( + widgets, + config_dict["microscopes"][microscope_name][ + hardware_ref_name + ], + ) + except: + widgets_value = [None] + microscope_tab.create_hardware_tab( + hardware_type, widgets, hardware_widgets_value=widgets_value + ) + else: + try: + widgets_value = [ + build_widgets_value( + widgets[1], + config_dict["microscopes"][microscope_name][ + hardware_ref_name + ], + ), + build_widgets_value( + widgets[2], + config_dict["microscopes"][microscope_name][ + hardware_ref_name + ], + ), + ] + except: + widgets_value = [[None], [None]] + microscope_tab.create_hardware_tab( + hardware_type, + hardware_widgets=widgets[1], + widgets=widgets[2], + top_widgets=widgets[0], + hardware_widgets_value=widgets_value[0], + constants_widgets_value=widgets_value[1], + ) + + def device_selected(self, event): + """Handle the event when a device is selected from the dropdown.""" + # # Get the selected device name + # selected_device_name = self.view.microscope_frame.get() + # # Find the key in the dictionary that corresponds to the selected value + # selected_key = next( + # key for key, value in device_types.items() + # if value == selected_device_name) + # print(f"Selected Device Key: {selected_key}") + # print(f"Selected Device Name: {selected_device_name}") + pass diff --git a/src/navigate/controller/sub_controllers/camera_setting_controller.py b/src/navigate/controller/sub_controllers/camera_setting_controller.py index 76415b145..bc4b5f71b 100644 --- a/src/navigate/controller/sub_controllers/camera_setting_controller.py +++ b/src/navigate/controller/sub_controllers/camera_setting_controller.py @@ -540,12 +540,12 @@ def update_camera_device_related_setting(self): self.trigger_active = camera_config_dict["trigger_active"] self.readout_speed = camera_config_dict["readout_speed"] # framerate_widgets - self.framerate_widgets["exposure_time"].widget.min = camera_config_dict[ - "exposure_time_range" - ]["min"] - self.framerate_widgets["exposure_time"].widget.max = camera_config_dict[ - "exposure_time_range" - ]["max"] + # self.framerate_widgets["exposure_time"].widget.min = camera_config_dict[ + # "exposure_time_range" + # ]["min"] + # self.framerate_widgets["exposure_time"].widget.max = camera_config_dict[ + # "exposure_time_range" + # ]["max"] # roi max width and height self.roi_widgets["Width"].widget.config(to=self.default_width) diff --git a/src/navigate/controller/sub_controllers/channel_setting_controller.py b/src/navigate/controller/sub_controllers/channel_setting_controller.py index 61714abc8..eb2b087bc 100644 --- a/src/navigate/controller/sub_controllers/channel_setting_controller.py +++ b/src/navigate/controller/sub_controllers/channel_setting_controller.py @@ -286,19 +286,19 @@ def update_setting_dict(setting_dict, widget_name): except Exception: setting_dict[widget_name] = 0 return False - ref_name = ( - "exposure_time" - if widget_name == "camera_exposure_time" - else widget_name - ) - setting_range = self.parent_controller.parent_controller.configuration[ - "configuration" - ]["gui"]["channels"][ref_name] - if ( - setting_dict[widget_name] < setting_range["min"] - or setting_dict[widget_name] > setting_range["max"] - ): - return False + # ref_name = ( + # "exposure_time" + # if widget_name == "camera_exposure_time" + # else widget_name + # ) + # setting_range = self.parent_controller.parent_controller.configuration[ + # "configuration" + # ]["gui"]["channels"][ref_name] + # if ( + # setting_dict[widget_name] < setting_range["min"] + # or setting_dict[widget_name] > setting_range["max"] + # ): + # return False else: setting_dict[widget_name] = channel_vals[widget_name].get() @@ -429,20 +429,20 @@ def verify_experiment_values(self): Warning info """ selected_channel_num = 0 - setting_range = self.configuration_controller.gui_setting["channels"] for channel_key in self.channel_setting_dict.keys(): setting_dict = self.channel_setting_dict[channel_key] + idx = int(channel_key[len("channel_"):]) - 1 if setting_dict["is_selected"]: selected_channel_num += 1 # laser power - if setting_dict["laser_power"] < setting_range["laser_power"]["min"]: + if setting_dict["laser_power"] < self.view.laserpower_pulldowns[idx]["from"]: return f"Laser power below configured threshold. Please adjust to meet or exceed the specified minimum in the configuration.yaml({setting_range['laser_power']['min']})." - elif setting_dict["laser_power"] > setting_range["laser_power"]["max"]: + elif setting_dict["laser_power"] > self.view.laserpower_pulldowns[idx]["to"]: return f"Laser power exceeds configured maximum. Please adjust to meet or be below the specified maximum in the configuration.yaml({setting_range['laser_power']['max']})." # exposure time - if setting_dict["camera_exposure_time"] < setting_range["exposure_time"]["min"]: + if setting_dict["camera_exposure_time"] < self.view.exptime_pulldowns[idx]["from"]: return f"Exposure time below configured threshold.Please adjust to meet or exceed the specified minimum in the configuration.yaml({setting_range['exposure_time']['min']})." - elif setting_dict["camera_exposure_time"] > setting_range["exposure_time"]["max"]: + elif setting_dict["camera_exposure_time"] > self.view.exptime_pulldowns[idx]["to"]: return f"Exposure time exceeds configured maximum. Please adjust to meet or be below the specified maximum in the configuration.yaml({setting_range['exposure_time']['max']})" if selected_channel_num == 0: diff --git a/src/navigate/controller/sub_controllers/channels_tab_controller.py b/src/navigate/controller/sub_controllers/channels_tab_controller.py index b32be536d..d6123d970 100644 --- a/src/navigate/controller/sub_controllers/channels_tab_controller.py +++ b/src/navigate/controller/sub_controllers/channels_tab_controller.py @@ -113,8 +113,6 @@ def __init__(self, view, parent_controller=None): self.z_origin = 0 #: float: The focus origin of the stack. self.focus_origin = 0 - #: float: The stage velocity. - self.stage_velocity = None #: float: The filter wheel delay. self.filter_wheel_delay = None #: dict: The microscope state dictionary. @@ -181,10 +179,9 @@ def initialize(self): config = self.parent_controller.configuration_controller self.stack_acq_widgets["cycling"].widget["values"] = ["Per Z", "Per Stack"] - self.stage_velocity = config.stage_setting_dict["velocity"] self.filter_wheel_delay = config.filter_wheel_setting_dict["filter_wheel_delay"] self.channel_setting_controller.initialize() - self.set_spinbox_range_limits(config.configuration["configuration"]["gui"]) + # self.set_spinbox_range_limits(config.configuration["configuration"]["gui"]) self.show_verbose_info("channels tab has been initialized") def populate_experiment_values(self): @@ -648,7 +645,7 @@ def update_timepoint_setting(self, call_parent=False): # time. Probably assemble a matrix of all the positions and then do # the calculations. - stage_delay = 0 # distance[max_distance_idx]/self.stage_velocity + stage_delay = 0 # TODO False value. # If we were actually acquiring the data, we would call the function to diff --git a/src/navigate/main.py b/src/navigate/main.py index 034758176..0b15de318 100644 --- a/src/navigate/main.py +++ b/src/navigate/main.py @@ -34,12 +34,12 @@ import tkinter as tk import platform import os -import warnings # Third Party Imports # Local Imports from navigate.controller.controller import Controller +from navigate.controller.configurator import Configurator from navigate.log_files.log_functions import log_setup from navigate.view.splash_screen import SplashScreen from navigate.tools.main_functions import ( @@ -68,7 +68,7 @@ def main(): --waveform_constants-path --rest-api-file --waveform-templates-file - --logging-config + --logging-confi Returns ------- @@ -78,12 +78,14 @@ def main(): -------- >>> python main.py --synthetic-hardware """ - if platform.system() != 'Windows': - print("WARNING: navigate was built to operate on a Windows platform. " - "While much of the software will work for evaluation purposes, some " - "unanticipated behaviors may occur. For example, it is known that the " - "Tkinter-based GUI does not grid symmetrically, nor resize properly " - "on MacOS. Testing on Linux operating systems has not been performed.") + if platform.system() != "Windows": + print( + "WARNING: navigate was built to operate on a Windows platform. " + "While much of the software will work for evaluation purposes, some " + "unanticipated behaviors may occur. For example, it is known that the " + "Tkinter-based GUI does not grid symmetrically, nor resize properly " + "on MacOS. Testing on Linux operating systems has not been performed." + ) # Start the GUI, withdraw main screen, and show splash screen. root = tk.Tk() @@ -106,20 +108,25 @@ def main(): rest_api_path, waveform_templates_path, logging_path, + configurator, ) = evaluate_parser_input_arguments(args) log_setup("logging.yml", logging_path) - Controller( - root, - splash_screen, - configuration_path, - experiment_path, - waveform_constants_path, - rest_api_path, - waveform_templates_path, - args, - ) + if args.configurator: + Configurator(root, splash_screen) + else: + Controller( + root, + splash_screen, + configuration_path, + experiment_path, + waveform_constants_path, + rest_api_path, + waveform_templates_path, + args, + ) + root.mainloop() diff --git a/src/navigate/model/devices/camera/__init__.py b/src/navigate/model/devices/camera/__init__.py index 368df1a98..b784ae042 100644 --- a/src/navigate/model/devices/camera/__init__.py +++ b/src/navigate/model/devices/camera/__init__.py @@ -1,3 +1,11 @@ """ Camera devices. -""" \ No newline at end of file +""" + +device_types = { + "hamamatsu_lightning": "Hamamatsu ORCA Lightning", + "hamamatsu_fire": "Hamamatsu ORCA Fire", + "hamamatsu_flash": "Hamamatsu Flash 4.0", + "photometics": "Photometrics Iris 15B", + "synthetic": "Virtual Device", +} diff --git a/src/navigate/model/devices/daq/__init__.py b/src/navigate/model/devices/daq/__init__.py index e10fb61c7..4324bdfdf 100644 --- a/src/navigate/model/devices/daq/__init__.py +++ b/src/navigate/model/devices/daq/__init__.py @@ -1,3 +1,5 @@ """ Data acquisition card devices. -""" \ No newline at end of file +""" + +device_types = {"ni": "National Instruments", "synthetic": "Virtual Device"} diff --git a/src/navigate/model/devices/filter_wheel/__init__.py b/src/navigate/model/devices/filter_wheel/__init__.py index 6002cd8d1..2839ffbde 100644 --- a/src/navigate/model/devices/filter_wheel/__init__.py +++ b/src/navigate/model/devices/filter_wheel/__init__.py @@ -1,3 +1,9 @@ """ Filter wheel devices. -""" \ No newline at end of file +""" + +device_types = { + "asi": "Applied Scientific Instrumentation", + "sutter": "Sutter Instruments", + "synthetic": "Virtual Device", +} diff --git a/src/navigate/model/devices/galvo/__init__.py b/src/navigate/model/devices/galvo/__init__.py index 9ffdceafb..e4a89bdc4 100644 --- a/src/navigate/model/devices/galvo/__init__.py +++ b/src/navigate/model/devices/galvo/__init__.py @@ -1,3 +1,5 @@ """ Galvanometer devices. -""" \ No newline at end of file +""" + +device_types = {"ni": "Analog Device", "synthetic": "Virtual Device"} diff --git a/src/navigate/model/devices/galvo/galvo_base.py b/src/navigate/model/devices/galvo/galvo_base.py index 37b40e432..74173041d 100644 --- a/src/navigate/model/devices/galvo/galvo_base.py +++ b/src/navigate/model/devices/galvo/galvo_base.py @@ -81,9 +81,7 @@ def __init__(self, microscope_name, device_connection, configuration, galvo_id=0 ]["daq"]["sample_rate"] #: float: Sweep time. - self.sweep_time = configuration["configuration"]["microscopes"][ - microscope_name - ]["daq"]["sweep_time"] + self.sweep_time = 0 #: float: Camera delay self.camera_delay = configuration["configuration"]["microscopes"][ diff --git a/src/navigate/model/devices/lasers/__init__.py b/src/navigate/model/devices/lasers/__init__.py index ddb463779..0c5141736 100644 --- a/src/navigate/model/devices/lasers/__init__.py +++ b/src/navigate/model/devices/lasers/__init__.py @@ -1,3 +1,5 @@ """ Laser devices. -""" \ No newline at end of file +""" + +device_types = {"ni": "Analog Device", "synthetic": "Virtual Device"} diff --git a/src/navigate/model/devices/mirrors/__init__.py b/src/navigate/model/devices/mirrors/__init__.py index a5d8c3579..70aa4cd77 100644 --- a/src/navigate/model/devices/mirrors/__init__.py +++ b/src/navigate/model/devices/mirrors/__init__.py @@ -1,3 +1,5 @@ """ Deformable mirror devices. -""" \ No newline at end of file +""" + +device_types = {"imop": "Imagine Optics", "synthetic": "Virtual Device"} diff --git a/src/navigate/model/devices/remote_focus/__init__.py b/src/navigate/model/devices/remote_focus/__init__.py index 4a2e563ba..3cb3e5094 100644 --- a/src/navigate/model/devices/remote_focus/__init__.py +++ b/src/navigate/model/devices/remote_focus/__init__.py @@ -1,3 +1,9 @@ """ Remote focus devices. -""" \ No newline at end of file +""" + +device_types = { + "equipment_solutions": "Equipment Solutions", + "ni": "Analog Device", + "synthetic": "Virtual Device", +} diff --git a/src/navigate/model/devices/remote_focus/remote_focus_base.py b/src/navigate/model/devices/remote_focus/remote_focus_base.py index 51adbd8a4..7480fabbb 100644 --- a/src/navigate/model/devices/remote_focus/remote_focus_base.py +++ b/src/navigate/model/devices/remote_focus/remote_focus_base.py @@ -80,9 +80,7 @@ def __init__(self, microscope_name, device_connection, configuration): ]["daq"]["sample_rate"] #: float: Sweep time of the DAQ. - self.sweep_time = configuration["configuration"]["microscopes"][ - microscope_name - ]["daq"]["sweep_time"] + self.sweep_time = 0 #: float: Camera delay percent. self.camera_delay = configuration["configuration"]["microscopes"][ diff --git a/src/navigate/model/devices/shutter/__init__.py b/src/navigate/model/devices/shutter/__init__.py index cb0ac8bdd..9c78de06e 100644 --- a/src/navigate/model/devices/shutter/__init__.py +++ b/src/navigate/model/devices/shutter/__init__.py @@ -1,3 +1,5 @@ """ Shutter devices. -""" \ No newline at end of file +""" + +device_types = {"ttl": "Analog/Digital Device", "synthetic": "Virtual Device"} diff --git a/src/navigate/model/devices/stages/__init__.py b/src/navigate/model/devices/stages/__init__.py index 0e4c1bb5e..c27a43f6d 100644 --- a/src/navigate/model/devices/stages/__init__.py +++ b/src/navigate/model/devices/stages/__init__.py @@ -1,3 +1,13 @@ """ Stage devices. -""" \ No newline at end of file +""" + +device_types = { + "asi": "Applied Scientific Instrumentation", + "galvo": "Analog/Digital Device", + "mcl": "Mad City Labs", + "pi": "Physik Instrumente", + "sutter": "Sutter Instruments", + "synthetic": "Virtual Device", + "tl_kcube_inertial": "ThorLabs KCube Inertial Device", +} diff --git a/src/navigate/model/devices/zoom/__init__.py b/src/navigate/model/devices/zoom/__init__.py index 76fa3a981..d19569bba 100644 --- a/src/navigate/model/devices/zoom/__init__.py +++ b/src/navigate/model/devices/zoom/__init__.py @@ -1,3 +1,8 @@ """ Zoom devices. -""" \ No newline at end of file +""" + +device_types = { + "dynamixel": "Dynamixel", + "synthetic": "Virtual Device", +} diff --git a/src/navigate/tools/main_functions.py b/src/navigate/tools/main_functions.py index bb271ee89..78ef28719 100644 --- a/src/navigate/tools/main_functions.py +++ b/src/navigate/tools/main_functions.py @@ -68,6 +68,8 @@ def evaluate_parser_input_arguments(args): Path to waveform templates file logging_path Path to non-default logging location + configurator + Boolean, True if configurator is enabled """ # Retrieve the Default Configuration paths ( @@ -79,6 +81,11 @@ def evaluate_parser_input_arguments(args): ) = get_configuration_paths() # Evaluate Input Arguments + if args.configurator: + configurator = True + else: + configurator = False + if args.config_file: assert args.config_file.exists(), "Configuration file Path {} not valid".format( args.config_file @@ -127,6 +134,7 @@ def evaluate_parser_input_arguments(args): rest_api_path, waveform_templates_path, logging_path, + configurator, ) @@ -146,6 +154,15 @@ def create_parser(): input_args = parser.add_argument_group("Input Arguments") + input_args.add_argument( + "-c", + "--configurator", + required=False, + default=False, + action="store_true", + help="Configurator - " "GUI for preparing a configuration.yaml file..", + ) + input_args.add_argument( "-sh", "--synthetic-hardware", diff --git a/src/navigate/view/configurator_application_window.py b/src/navigate/view/configurator_application_window.py new file mode 100644 index 000000000..884cc462e --- /dev/null +++ b/src/navigate/view/configurator_application_window.py @@ -0,0 +1,572 @@ +# Copyright (c) 2021-2022 The University of Texas Southwestern Medical Center. +# All rights reserved. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted for academic and research use only (subject to the +# limitations in the disclaimer below) provided that the following conditions are met: + +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. + +# * Neither the name of the copyright holders nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. + +# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# Standard Library Imports +import tkinter as tk +from tkinter import ttk, simpledialog +import logging +from pathlib import Path +import importlib + +# Third Party Imports + +# Local Imports +from navigate.view.custom_widgets.DockableNotebook import DockableNotebook +from navigate.view.custom_widgets.CollapsibleFrame import CollapsibleFrame + +# Logger Setup +p = __name__.split(".")[1] + +widget_types = { + "Combobox": ttk.Combobox, + "Input": ttk.Entry, + "Spinbox": ttk.Spinbox, + "Checkbutton": ttk.Checkbutton, + "Button": ttk.Button, +} + +variable_types = { + "string": tk.StringVar, + "float": tk.DoubleVar, + "bool": tk.BooleanVar, + "int": tk.IntVar, +} + + +class ConfigurationAssistantWindow(ttk.Frame): + def __init__(self, root, *args, **kwargs): + """Initiates the main application window + + Parameters + ---------- + root : tk.Tk + The main window of the application + *args + Variable length argument list + **kwargs + Arbitrary keyword arguments + """ + #: tk.Tk: The main window of the application + self.root = root + self.root.title("Configuration Assistant") + + ttk.Frame.__init__(self, self.root, *args, **kwargs) + + #: logging.Logger: The logger for this class + self.logger = logging.getLogger(p) + + view_directory = Path(__file__).resolve().parent + try: + photo_image = view_directory.joinpath("icon", "mic.png") + self.root.iconphoto(True, tk.PhotoImage(file=photo_image)) + except tk.TclError: + pass + + self.root.resizable(False, False) + self.root.geometry("") + self.root.rowconfigure(0, weight=1) + self.root.columnconfigure(0, weight=1) + + #: ttk.Frame: The top frame of the application + self.top_frame = ttk.Frame(self.root) + + #: ttk.Frame: The main frame of the application + self.microscope_frame = ttk.Frame(self.root) + + self.grid(column=0, row=0, sticky=tk.NSEW) + self.top_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=3, pady=3) + self.microscope_frame.grid( + row=1, column=0, columnspan=5, sticky=tk.NSEW, padx=3, pady=3 + ) + + #: ttk.Frame: The top frame of the application + self.top_window = TopWindow(self.top_frame, self.root) + + +class TopWindow(ttk.Frame): + """Top Frame for Configuration Assistant. + + This class is the initial window for the configurator application. + It contains the following: + - Entry for number of configurations + - Continue button + - Cancel button + """ + + def __init__(self, main_frame, root, *args, **kwargs): + """Initialize Top Frame. + + Parameters + ---------- + main_frame : ttk.Frame + Window to place widgets in. + root : tk.Tk + Root window of the application. + *args + Variable length argument list. + **kwargs + Arbitrary keyword arguments. + """ + + #: ttk.Frame: The main frame of the application + self.microscope_frame = main_frame + ttk.Frame.__init__(self, self.microscope_frame, *args, **kwargs) + + # Formatting + tk.Grid.columnconfigure(self, "all", weight=1) + tk.Grid.rowconfigure(self, "all", weight=1) + + self.new_button = tk.Button(root, text="New Configuration") + self.new_button.grid(row=0, column=0, sticky=tk.NE, padx=3, pady=(10, 1)) + self.new_button.config(width=15) + + self.load_button = tk.Button(root, text="Load Configuration") + self.load_button.grid(row=0, column=1, sticky=tk.NE, padx=3, pady=(10, 1)) + self.load_button.config(width=15) + + self.add_button = tk.Button(root, text="Add A Microscope") + self.add_button.grid(row=0, column=2, sticky=tk.NE, padx=3, pady=(10, 1)) + self.add_button.config(width=15) + + self.save_button = tk.Button(root, text="Save") + self.save_button.grid(row=0, column=3, sticky=tk.NE, padx=3, pady=(10, 1)) + self.save_button.config(width=15) + + #: tk.Button: The button to cancel the application. + self.cancel_button = tk.Button(root, text="Cancel") + self.cancel_button.grid(row=0, column=4, sticky=tk.NE, padx=3, pady=(10, 1)) + self.cancel_button.config(width=15) + + +class MicroscopeWindow(DockableNotebook): + def __init__(self, frame, root, *args, **kwargs): + """Initialize Microscope Frame. + + Parameters + ---------- + main_frame : ttk.Frame + Window to place widgets in. + root : tk.Tk + Root window of the application. + *args + Variable length argument list. + **kwargs + Arbitrary keyword arguments. + """ + + DockableNotebook.__init__(self, frame, root, *args, **kwargs) + self.grid(row=0, column=0, sticky=tk.NSEW) + + self.menu.delete("Popout Tab") + self.menu.add_command(label="Rename", command=self.rename_microscope) + self.menu.add_command(label="Delete", command=self.delete_microscope) + + def rename_microscope(self): + """Rename microscope""" + + result = simpledialog.askstring("Input", "Enter microscope name:") + if result: + tab = self.select() + tab_name = self.tab(tab)["text"] + self.tab(tab, text=result) + self.tab_list.remove(tab_name) + self.tab_list.append(result) + + def delete_microscope(self): + """Delete selected microscope""" + tab = self.select() + tab_name = self.tab(tab)["text"] + current_tab_index = self.index("current") + if current_tab_index >= 0: + self.forget(current_tab_index) + self.tab_list.remove(tab_name) + + +class MicroscopeTab(DockableNotebook): + def __init__(self, parent, root, *args, **kwargs): + """Initialize Microscope Tab. + + Parameters + ---------- + main_frame : ttk.Frame + Window to place widgets in. + root : tk.Tk + Root window of the application. + *args + Variable length argument list. + **kwargs + Arbitrary keyword arguments. + """ + + # Init Frame + DockableNotebook.__init__(self, parent, root, *args, **kwargs) + + # Formatting + tk.Grid.columnconfigure(self, "all", weight=1) + tk.Grid.rowconfigure(self, "all", weight=1) + + def create_hardware_tab( + self, name, hardware_widgets, widgets=None, top_widgets=None, **kwargs + ): + """Create hardware tab + + Parameters + ---------- + name : str + tab name/hardware name + hardware_widgets : dict + hardware widgets dict + widgets : dict + constants widgets dict + top_widgets : dict + button widgets dict + *args + Variable length argument list. + **kwargs + Arbitrary keyword arguments + """ + tab = HardwareTab( + name, hardware_widgets, widgets=widgets, top_widgets=top_widgets, **kwargs + ) + self.tab_list.append(name) + self.add(tab, text=name, sticky=tk.NSEW) + + +class HardwareTab(ttk.Frame): + def __init__( + self, + name, + hardware_widgets, + *args, + widgets=None, + top_widgets=None, + hardware_widgets_value=[None], + constants_widgets_value=[None], + **kwargs + ): + """Initialize Microscope Tab. + + Parameters + ---------- + name : str + tab name/hardware name + hardware_widgets : dict + hardware widgets dict + widgets : dict + constants widgets dict + top_widgets : dict + button widgets dict + hardware_widgets_value : list[dict] + list of values for hardware widgets + constants_widgets_value : list[dict] + list of values for constants widgets + *args + Variable length argument list. + **kwargs + Arbitrary keyword arguments + """ + # Init Frame + tk.Frame.__init__(self, *args, **kwargs) + + self.name = name + + # Formatting + tk.Grid.columnconfigure(self, "all", weight=1) + tk.Grid.rowconfigure(self, "all", weight=1) + scroll_frame = ttk.Frame(self) + scroll_frame.grid(row=3, column=0, sticky=tk.NSEW) + canvas = tk.Canvas(scroll_frame, width=1000, height=500) + scrollbar = ttk.Scrollbar(scroll_frame, orient="vertical", command=canvas.yview) + content_frame = ttk.Frame(canvas) + + content_frame.bind( + "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) + ) + + canvas.create_window((0, 0), window=content_frame, anchor="nw") + + canvas.configure(yscrollcommand=scrollbar.set) + + canvas.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") + + self.top_frame = ttk.Frame(content_frame) + + self.top_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=10) + + self.hardware_frame = ttk.Frame(content_frame) + self.hardware_frame.grid(row=1, column=0, sticky=tk.NSEW, padx=10) + + self.bottom_frame = ttk.Frame(content_frame) + self.bottom_frame.grid(row=2, column=0, sticky=tk.NSEW, padx=10) + self.frame_row = 0 + self.row_offset = self.frame_row + 1 + + self.variables = {} + self.values_dict = {} + self.variables_list = [] + + self.build_widgets(top_widgets, parent=self.top_frame) + + for widgets_value in hardware_widgets_value: + self.build_widgets( + hardware_widgets, + parent=self.hardware_frame, + widgets_value=widgets_value, + ) + + count = 0 + for widgets_value in constants_widgets_value: + self.build_widgets(widgets, widgets_value=widgets_value) + # if self.name in ["Filter Wheel"]: + # count += 1 + # print("building widgets value:", self.name, widgets_value) + # if count > 4: + + # if count >= 10: + # break + + def create_hardware_widgets(self, hardware_widgets, frame, direction="vertical"): + """create widgets + + Parameters + ---------- + hardware_widgets : dict + name: (display_name, widget_type, value_type, values, info) + frame : tk.Frame + the parent frame for widgets + direction : str + direction of the widget layouts + """ + if hardware_widgets is None: + return + if type(frame) is CollapsibleFrame: + content_frame = frame.content_frame + else: + content_frame = frame + i = 0 + for k, v in hardware_widgets.items(): + if k == "frame_config": + continue + if v[1] == "Label": + label = ttk.Label(content_frame, text=v[0]) + label.grid(row=i, column=0, sticky=tk.NW, padx=3) + seperator = ttk.Separator(content_frame) + seperator.grid(row=i + 1, columnspan=2, sticky=tk.NSEW, padx=3) + i += 2 + continue + elif v[1] != "Button": + self.variables[k] = variable_types[v[2]]() + label_text = v[0] + " :" if v[0][-1] != ":" else v[0] + label = ttk.Label(content_frame, text=label_text) + if direction == "vertical": + label.grid(row=i, column=0, sticky=tk.NW, padx=(3, 10), pady=3) + else: + label.grid(row=0, column=i, sticky=tk.NW, padx=(5, 3), pady=3) + i += 1 + if v[1] == "Checkbutton": + widget = widget_types[v[1]]( + content_frame, text="", variable=self.variables[k] + ) + else: + widget = widget_types[v[1]]( + content_frame, textvariable=self.variables[k], width=30 + ) + if v[1] == "Combobox": + if type(v[3]) == list: + v[3] = dict([(t, t) for t in v[3]]) + self.values_dict[k] = v[3] + temp = list(v[3].keys()) + widget.config(values=temp) + if v[2] == "bool": + widget.set(str(temp[-1])) + else: + widget.set(temp[-1]) + elif v[1] == "Spinbox": + if type(v[3]) != dict: + v[3] = {} + widget.config(from_=v[3].get("from", 0)) + widget.config(to=v[3].get("to", 100000)) + widget.config(increment=v[3].get("step", 1)) + widget.set(v[3].get("from", 0)) + else: + widget = ttk.Button( + content_frame, + text=v[0], + command=self.build_event_handler( + hardware_widgets, k, frame, self.frame_row + ), + ) + if direction == "vertical": + widget.grid(row=i, column=1, sticky=tk.NSEW, padx=5, pady=3) + else: + widget.grid(row=0, column=i, sticky=tk.NW, padx=(10, 3), pady=(3, 0)) + + if len(v) >= 5 and v[4]: + label = ttk.Label(content_frame, text=v[4]) + if direction == "vertical": + label.grid(row=i, column=2, sticky=tk.NW, padx=(10, 10), pady=3) + else: + label.grid(row=1, column=i, sticky=tk.NW, padx=(10, 3), pady=0) + i += 1 + + def build_widgets(self, widgets, *args, parent=None, widgets_value=None, **kwargs): + """Build widgets + + Parameters + ---------- + widgets : dict + widget dict + parent : frame + parent frame to put widgets + widgets_value : dict + valude dict of widgets + *args + Variable length argument list. + **kwargs + Arbitrary keyword arguments, ref="reference name", direction="vertical" + """ + if not widgets: + return + if parent is None: + parent = self.bottom_frame + collapsible = False + title = "Hardware" + format = None + temp_ref = None + direction = "vertical" + if "frame_config" in widgets: + collapsible = widgets["frame_config"].get("collapsible", False) + title = widgets["frame_config"].get("title", "Hardware") + format = widgets["frame_config"].get("format", None) + temp_ref = widgets["frame_config"].get("ref", None) + direction = widgets["frame_config"].get("direction", "vertical") + if collapsible: + self.foldAllFrames() + frame = CollapsibleFrame(parent=parent, title=title) + # only display one callapsible frame at a time + frame.label.bind("", self.create_toggle_function(frame)) + else: + frame = ttk.Frame(parent) + frame.grid(row=self.frame_row, column=0, sticky=tk.NSEW, padx=20) + self.frame_row += 1 + + ref = None + if kwargs: + ref = kwargs.get("ref", None) + direction = kwargs.get("direction", "vertical") + ref = ref or temp_ref + self.variables = {} + self.values_dict = {} + self.variables_list.append((self.variables, self.values_dict, ref, format)) + self.create_hardware_widgets(widgets, frame=frame, direction=direction) + + if widgets_value: + for k, v in widgets_value.items(): + try: + if k == "axes": + print("*** type", type(v), v, str(v)) + self.variables[k].set(str(v)) + except (TypeError, ValueError): + pass + except tk._tkinter.TclError: + pass + + + def foldAllFrames(self, except_frame=None): + """Fold all collapsible frames except one frame + + Parameters + ---------- + except_frame : tk.Frame + the unfold frame + """ + for child in self.hardware_frame.winfo_children(): + if isinstance(child, CollapsibleFrame) and child is not except_frame: + child.fold() + for child in self.bottom_frame.winfo_children(): + if isinstance(child, CollapsibleFrame) and child is not except_frame: + child.fold() + + def create_toggle_function(self, frame): + """Toggle collapsible frame + + Parameters + ---------- + frame : tk.Frame + the frame to toggle + """ + def func(event): + self.foldAllFrames(frame) + frame.toggle_visibility() + + return func + + def build_event_handler(self, hardware_widgets, key, frame, frame_id): + """Build button event handler + + Parameters + ---------- + hardware_widgets : dict + widget dict containing the button + key : str + reference of the button + frame : tk.Frame + the frame to put/delete widgets + frame_id : int + index of the frame + """ + def func(*args, **kwargs): + v = hardware_widgets[key] + if "widgets" in v[2]: + if "parent" in v[2]: + parent = ( + self.hardware_frame + if v[2]["parent"].startswith("hardware") + else None + ) + else: + parent_id = frame.winfo_parent() + parent = self.nametowidget(parent_id) + widgets = ( + hardware_widgets if v[2]["widgets"] == "self" else v[2]["widgets"] + ) + self.build_widgets( + widgets, + parent=parent, + ref=v[2].get("ref", None), + direction=v[2].get("direction", "vertical"), + ) + # collaps other frame + elif v[2].get("delete", False): + frame.grid_remove() + self.variables_list[frame_id - self.row_offset] = None + + return func diff --git a/src/navigate/view/custom_widgets/CollapsibleFrame.py b/src/navigate/view/custom_widgets/CollapsibleFrame.py new file mode 100644 index 000000000..f6236f2e4 --- /dev/null +++ b/src/navigate/view/custom_widgets/CollapsibleFrame.py @@ -0,0 +1,33 @@ +import tkinter as tk + +class CollapsibleFrame(tk.Frame): + def __init__(self, parent, title="", *args, **kwargs): + tk.Frame.__init__(self, parent, *args, **kwargs) + + self.title = title + self.visible = False + + # Create a label to act as a title/header + self.label = tk.Label(self, text=self.title, bg="lightgrey", relief="raised", padx=5) + self.label.grid(row=0, column=0, sticky=tk.NSEW) + + # Create a frame to hold the contents of the collapsible frame + self.content_frame = tk.Frame(self) + self.toggle_visibility() + + self.label.bind("", lambda event: self.toggle_visibility()) + + def toggle_visibility(self): + if self.visible: + self.label["text"] = self.title + " " + "\u25BC" + self.content_frame.grid_forget() # Hide the content frame + self.visible = False + else: + self.label["text"] = self.title + " " + "\u25B2" + self.content_frame.grid(row=1, column=0, sticky=tk.NSEW) # Show the content frame + self.visible = True + + def fold(self): + if self.visible: + self.toggle_visibility() + diff --git a/src/navigate/view/main_application_window.py b/src/navigate/view/main_application_window.py index dc2986848..acb964282 100644 --- a/src/navigate/view/main_application_window.py +++ b/src/navigate/view/main_application_window.py @@ -59,7 +59,7 @@ class MainApp(ttk.Frame): Adds the options for each file menu. It then sets up the frames, then grids the frames. - Finally it uses the notebook classes to put them into the respective frames on the + Finally, it uses the notebook classes to put them into the respective frames on the tk.Grid. Each of the notebook classes includes tab classes and inits those etc. The second parameter in each classes __init__ function is the parent. @@ -108,12 +108,9 @@ def __init__(self, root, *args, **kwargs): ttk.Frame.__init__(self, self.scroll_frame.interior, *args, **kwargs) - # Initialize Logger #: logging.Logger: The logger for this class self.logger = logging.getLogger(p) - # This starts the main window config, and makes sure that any child - # widgets can be resized with the window #: tk.Tk: The main window of the application self.root = root self.root.title("navigate") @@ -125,8 +122,10 @@ def __init__(self, root, *args, **kwargs): self.root.iconphoto(True, tk.PhotoImage(file=photo_image)) except tk.TclError: pass + self.root.resizable(True, True) self.root.geometry("") + tk.Grid.columnconfigure(root, "all", weight=1) tk.Grid.rowconfigure(root, "all", weight=1) @@ -159,11 +158,11 @@ def __init__(self, root, *args, **kwargs): self.frame_top_right.grid(row=1, column=1, sticky=tk.NSEW, padx=3, pady=3) self.frame_bottom_right.grid(row=2, column=1, sticky=tk.NSEW, padx=3, pady=3) - # Putting Notebooks into frames, tabs are held within the class of each - # notebook #: SettingsNotebook: The settings notebook for the application self.settings = SettingsNotebook(self.frame_left, self.root) + #: CameraNotebook: The camera notebook for the application self.camera_waveform = CameraNotebook(self.frame_top_right, self.root) + #: AcquireBar: The acquire bar for the application self.acqbar = AcquireBar(self.top_frame, self.root) diff --git a/src/navigate/view/main_window_content/channels_tab.py b/src/navigate/view/main_window_content/channels_tab.py index 0474a9f1f..58a1ccef5 100644 --- a/src/navigate/view/main_window_content/channels_tab.py +++ b/src/navigate/view/main_window_content/channels_tab.py @@ -289,9 +289,9 @@ def populate_frame(self, channels): ValidatedSpinbox( self.frame_columns[4], from_=0, - to=5000.0, + to=1000.0, textvariable=self.exptime_variables[num], - increment=25, + increment=5, width=5, font=tk.font.Font(size=11), ) @@ -306,9 +306,9 @@ def populate_frame(self, channels): ValidatedSpinbox( self.frame_columns[5], from_=0, - to=5000.0, + to=1000.0, textvariable=self.interval_variables[num], - increment=1, + increment=5, width=3, font=tk.font.Font(size=11), ) @@ -394,7 +394,7 @@ def __init__(self, settings_tab, *args, **kwargs): label=start_labels[i], input_class=ValidatedSpinbox, input_var=tk.DoubleVar(), - input_args={"from_": 0.0, "to": 10000, "increment": 0.5, "width": 6}, + input_args={"from_": -5000, "to": 10000, "increment": 1, "width": 6}, ) self.inputs[start_names[i]].grid( row=i + 1, column=0, sticky="N", pady=2, padx=(6, 0) @@ -420,7 +420,7 @@ def __init__(self, settings_tab, *args, **kwargs): label=end_labels[i], input_class=ValidatedSpinbox, input_var=tk.DoubleVar(), - input_args={"from_": 0.0, "to": 10000, "increment": 0.5, "width": 6}, + input_args={"from_": -5000, "to": 10000, "increment": 1, "width": 6}, ) self.inputs[end_names[i]].grid( row=i + 1, column=1, sticky="N", pady=2, padx=(6, 0) @@ -439,7 +439,7 @@ def __init__(self, settings_tab, *args, **kwargs): parent=self.pos_slice, input_class=ValidatedSpinbox, input_var=tk.DoubleVar(), - input_args={"width": 6}, + input_args={"from_": 0.1, "to": 1000, "increment": 0.1, "width": 6}, ) self.inputs["step_size"].grid(row=1, column=2, sticky="N", padx=6) @@ -455,7 +455,7 @@ def __init__(self, settings_tab, *args, **kwargs): label=slice_labels[i], input_class=ttk.Spinbox, input_var=tk.DoubleVar(), - input_args={"increment": 0.5, "width": 6}, + input_args={"from_": 0.1, "to": 1000, "increment": 0.1, "width": 6}, ) self.inputs[slice_names[i]].widget.configure(state="disabled") self.inputs[slice_names[i]].grid( @@ -603,7 +603,7 @@ def __init__(self, settings_tab, *args, **kwargs): from_=0, to=5000.0, textvariable=self.stack_acq_spinval, # this holds the data in the entry - increment=25, + increment=1, width=6, ) self.stack_acq_spinbox.grid(row=2, column=1, sticky=tk.NSEW, pady=2) @@ -623,7 +623,7 @@ def __init__(self, settings_tab, *args, **kwargs): from_=0, to=5000.0, textvariable=self.stack_pause_spinval, - increment=25, + increment=1, width=6, ) self.stack_pause_spinbox.grid(row=0, column=3, sticky=tk.NSEW, pady=2) diff --git a/test/config/test_config.py b/test/config/test_config.py index b1b8c268c..9fae9a515 100644 --- a/test/config/test_config.py +++ b/test/config/test_config.py @@ -78,6 +78,7 @@ def test_config_methods(): "verify_waveform_constants", "verify_configuration", "yaml", + "build_ref_name" ] for method in methods: assert method in desired_methods diff --git a/test/controller/sub_controllers/test_camera_setting_controller.py b/test/controller/sub_controllers/test_camera_setting_controller.py index 260534b02..3d9afbf46 100644 --- a/test/controller/sub_controllers/test_camera_setting_controller.py +++ b/test/controller/sub_controllers/test_camera_setting_controller.py @@ -115,14 +115,6 @@ def test_init(self): assert self.camera_settings.mode_widgets["Pixels"].widget.cget("increment") == 1 # Framerate - assert ( - self.camera_settings.framerate_widgets["exposure_time"].widget.min - == camera_config_dict["exposure_time_range"]["min"] - ) - assert ( - self.camera_settings.framerate_widgets["exposure_time"].widget.max - == camera_config_dict["exposure_time_range"]["max"] - ) assert ( str(self.camera_settings.framerate_widgets["exposure_time"].widget["state"]) == "disabled" diff --git a/test/model/devices/galvo/test_galvo_base.py b/test/model/devices/galvo/test_galvo_base.py index 4be9e0910..066fad05d 100644 --- a/test/model/devices/galvo/test_galvo_base.py +++ b/test/model/devices/galvo/test_galvo_base.py @@ -85,7 +85,7 @@ def test_galvo_base_initialization(self): assert self.galvo.microscope_name == "Mesoscale" assert self.galvo.galvo_name == "Galvo 0" assert self.galvo.sample_rate == 100000 - assert self.galvo.sweep_time == 0.2 + assert self.galvo.camera_delay == self.configuration["configuration"]["microscopes"][ self.microscope_name ]["camera"]["delay"] / 1000 diff --git a/test/model/devices/galvo/test_galvo_ni.py b/test/model/devices/galvo/test_galvo_ni.py index 77c5a425b..1132e6aad 100644 --- a/test/model/devices/galvo/test_galvo_ni.py +++ b/test/model/devices/galvo/test_galvo_ni.py @@ -83,7 +83,6 @@ def test_galvo_ni_initialization(self): assert self.galvo.microscope_name == "Mesoscale" assert self.galvo.galvo_name == "Galvo 0" assert self.galvo.sample_rate == 100000 - assert self.galvo.sweep_time == 0.2 assert self.galvo.camera_delay == self.configuration["configuration"]["microscopes"][ self.microscope_name ]["camera"]["delay"] / 1000