from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
    QApplication,
    QMainWindow,
    QWidget,
    QVBoxLayout,
    QAction,
    QDialog,
    QLabel,
    QLineEdit,
    QPushButton,
    QHBoxLayout,
    QListWidget,
    QListWidgetItem,
    QMessageBox,
    QFormLayout,
    QAbstractItemView,
    QDial,
    QMenuBar,
    QGroupBox,
    QFileDialog,
)
import sys
import threading
import time
import re
import serial
import numpy as np
from PyQt5 import QtWidgets, QtCore
import pyqtgraph as pg

# Import the official Lake Shore drivers
try:
    from lakeshore.model_224 import Model224
    from lakeshore.model_372 import Model372
except ImportError:
    # If the lakeshore package is not installed, define None
    Model224 = None
    Model372 = None

# ============================ Device Classes ============================= #

# ------ CTC100 Class ------ #


class CTC100:
    """
    Class to interact with the CTC100 Programmable Temperature Controller.
    """

    def __init__(self, address, channels=None):
        """
        Initialize CTC100 device.

        :param address: Serial port address (e.g., 'COM3').
        :param channels: List of channels to read from (e.g., [1, 2, 3]).
        """
        self.address = address
        self.channels = channels or [1]  # Default to channel 1
        self.device = serial.Serial(address, baudrate=115200, timeout=1)

    def close(self):
        self.device.close()

    def write(self, command):
        self.device.write((command + "\n").encode())  # \n terminates commands
        t1 = time.time()
        response = self.device.read()
        while len(response) == 0 or response[-2:] != b"\r\n":
            response += self.device.read()
            t2 = time.time()
            if (t2 - t1) > 1:
                break
        return response

    def get_variable(self, var):
        var = var.replace(" ", "")
        return self.write("{}?".format(var))

    def set_variable(self, var, val):
        var = var.replace(" ", "")
        val = "({})".format(val)
        return self.write("{} = {}".format(var, val))

    def read(self, channel):
        if not isinstance(channel, str):
            channel = "In{}".format(channel)
        response = self.get_variable("{}.value".format(channel))

        # Extract the response using a regex
        match = re.search(
            r"[-+]?\d*\.\d+([eE][-+]?\d+)?", response.decode("utf-8")
        )

        if match is not None:
            return float(match.group())
        else:
            raise RuntimeError(f"Unable to read from channel {channel}")

    def read_all_channels(self):
        """
        Read temperature values from all specified channels.

        :return: Dictionary with channel numbers as keys and readings as values.
        """
        readings = {}
        for channel in self.channels:
            try:
                temp = self.read(channel)
                readings[channel] = temp
            except Exception as e:
                print(
                    f"Error reading channel {channel} on CTC100 ({self.address}): {e}"
                )
                readings[channel] = None
        return readings

    def enableHeater(self):
        self.write("outputEnable on")

    def setHeaterOutput(self, output_value):
        """
        Set the heater output percentage (0-100%).
        """
        self.set_variable("Out1.Heater.Range", output_value)


# ------ Simulated CTC100 Class ------ #
class SimulatedCTC100:
    """
    Simulated CTC100 device for testing without hardware.
    """

    def __init__(self, address, channels=None):
        """
        Initialize simulated CTC100 device.

        :param address: Device identifier.
        :param channels: List of channels to simulate.
        """
        self.address = address
        self.channels = channels or [1]
        self.temperatures = {channel: 300.0 for channel in self.channels}
        self.heater_output = 0.0
        print(
            f"Simulated CTC100 initialized at {address} with channels {self.channels}"
        )

    def read_all_channels(self):
        """
        Simulate reading temperature values from all channels.

        :return: Dictionary with channel numbers as keys and readings as values.
        """
        readings = {}
        for channel in self.channels:
            # Simulate temperature fluctuations
            fluctuation = np.random.normal(scale=0.1)
            self.temperatures[channel] += (self.heater_output -
                                           50) * 0.01 + fluctuation
            readings[channel] = self.temperatures[channel]
        return readings

    def read(self, channel):
        # Simulate temperature reading
        if channel in self.channels:
            temp = self.temperatures[channel]
            return temp
        else:
            raise RuntimeError(
                f"Channel {channel} not found in simulated device.")

    def enableHeater(self):
        pass  # No action needed for simulation

    def setHeaterOutput(self, output_value):
        self.heater_output = float(output_value)


# ------ LakeShore224Device Class ------ #
class LakeShore224Device:
    """
    Class to interact with the Lake Shore 224 Temperature Monitor using the official driver.
    """

    def __init__(self, port, channels=None):
        try:
            if Model224 is not None:
                self.device = Model224(com_port=port)
            else:
                raise ImportError("Lake Shore Model224 driver not available.")
            self.channels = channels or [1]
            print(
                f"Connected to Lake Shore 224 on {port} with channels {self.channels}")
        except Exception as e:
            raise e

    def get_temperature(self, channel=1):
        try:
            temp = self.device.get_kelvin_reading(channel)
            return temp
        except Exception as e:
            print(
                f"Error reading temperature from Lake Shore 224 (Channel {channel}): {e}"
            )
            return None

    def read_all_channels(self):
        readings = {}
        for channel in self.channels:
            temp = self.get_temperature(channel)
            readings[channel] = temp
        return readings


# ------ Simulated LakeShore224Device ------ #
class SimulatedLakeShore224Device:
    """
    Simulated Lake Shore 224 device for testing without hardware.
    """

    def __init__(self, port, channels=None):
        self.port = port
        self.channels = channels or [1]
        self.temperatures = {channel: 77.0 for channel in self.channels}
        print(
            f"Simulated Lake Shore 224 initialized at {port} with channels {self.channels}"
        )

    def get_temperature(self, channel=1):
        # Simulate temperature fluctuations
        fluctuation = np.random.normal(scale=0.05)
        self.temperatures[channel] += fluctuation
        return self.temperatures[channel]

    def read_all_channels(self):
        readings = {}
        for channel in self.channels:
            temp = self.get_temperature(channel)
            readings[channel] = temp
        return readings


# ------ LakeShore372Device Class ------ #
class LakeShore372Device:
    """
    Class to interact with the Lake Shore 372 Temperature Controller using the official driver.
    """

    def __init__(self, port, channels=None):
        try:
            if Model372 is not None:
                self.device = Model372(com_port=port)
            else:
                raise ImportError("Lake Shore Model372 driver not available.")
            self.channels = channels or [1]
            print(
                f"Connected to Lake Shore 372 on {port} with channels {self.channels}")
        except Exception as e:
            raise e

    def get_temperature(self, channel=1):
        try:
            temp = self.device.get_kelvin_reading(channel)
            return temp
        except Exception as e:
            print(
                f"Error reading temperature from Lake Shore 372 (Channel {channel}): {e}"
            )
            return None

    def read_all_channels(self):
        readings = {}
        for channel in self.channels:
            temp = self.get_temperature(channel)
            readings[channel] = temp
        return readings

    def set_heater_output(self, heater_number=1, heat_percent=0.0):
        try:
            # Setting heater output percentage according to the official driver methods
            self.device.set_heater_output_percentage(heat_percent)
            return True
        except Exception as e:
            print(f"Error setting heater output on Lake Shore 372: {e}")
            return False


# ------ Simulated LakeShore372Device ------ #
class SimulatedLakeShore372Device:
    """
    Simulated Lake Shore 372 device for testing without hardware.
    """

    def __init__(self, port, channels=None):
        self.port = port
        self.channels = channels or [1]
        self.temperatures = {channel: 4.2 for channel in self.channels}
        self.heater_output = 0.0
        print(
            f"Simulated Lake Shore 372 initialized at {port} with channels {self.channels}"
        )

    def get_temperature(self, channel=1):
        # Simulate temperature fluctuations
        fluctuation = np.random.normal(scale=0.02)
        self.temperatures[channel] += (self.heater_output -
                                       50) * 0.005 + fluctuation
        return self.temperatures[channel]

    def read_all_channels(self):
        readings = {}
        for channel in self.channels:
            temp = self.get_temperature(channel)
            readings[channel] = temp
        return readings

    def set_heater_output(self, heater_number=1, heat_percent=0.0):
        self.heater_output = heat_percent
        return True

# ============================ GUI Application ============================= #


class ConfigurationDialog(QtWidgets.QDialog):
    def __init__(self, parent, ctc100_devices, ls224_devices, ls372_devices):
        super().__init__(parent)
        self.setWindowTitle("Configure Tabs and Thermometers")
        self.ctc100_devices = ctc100_devices
        self.ls224_devices = ls224_devices
        self.ls372_devices = ls372_devices

        self.config = []  # Configuration data
        self.initUI()

    def initUI(self):
        layout = QtWidgets.QVBoxLayout()

        # Tabs List
        self.tabs_list = QtWidgets.QListWidget()
        layout.addWidget(self.tabs_list)

        # Buttons to add, edit, delete tabs
        btn_layout = QtWidgets.QHBoxLayout()
        add_tab_btn = QtWidgets.QPushButton("Add Tab")
        edit_tab_btn = QtWidgets.QPushButton("Edit Tab")
        delete_tab_btn = QtWidgets.QPushButton("Delete Tab")
        btn_layout.addWidget(add_tab_btn)
        btn_layout.addWidget(edit_tab_btn)
        btn_layout.addWidget(delete_tab_btn)
        layout.addLayout(btn_layout)

        add_tab_btn.clicked.connect(self.add_tab)
        edit_tab_btn.clicked.connect(self.edit_tab)
        delete_tab_btn.clicked.connect(self.delete_tab)

        # OK and Cancel buttons
        ok_cancel_layout = QtWidgets.QHBoxLayout()
        ok_btn = QtWidgets.QPushButton("OK")
        cancel_btn = QtWidgets.QPushButton("Cancel")
        ok_cancel_layout.addWidget(ok_btn)
        ok_cancel_layout.addWidget(cancel_btn)
        layout.addLayout(ok_cancel_layout)

        ok_btn.clicked.connect(self.accept)
        cancel_btn.clicked.connect(self.reject)

        self.setLayout(layout)

        self.update_tabs_list()

    def update_tabs_list(self):
        self.tabs_list.clear()
        for tab in self.config:
            self.tabs_list.addItem(tab['tab_name'])

    def add_tab(self):
        tab_config_dialog = TabConfigDialog(
            self, self.ctc100_devices, self.ls224_devices, self.ls372_devices)
        if tab_config_dialog.exec_():
            new_tab_config = tab_config_dialog.get_config()
            self.config.append(new_tab_config)
            self.update_tabs_list()

    def edit_tab(self):
        selected_items = self.tabs_list.selectedItems()
        if selected_items:
            index = self.tabs_list.row(selected_items[0])
            tab_config = self.config[index]
            tab_config_dialog = TabConfigDialog(
                self, self.ctc100_devices, self.ls224_devices, self.ls372_devices, tab_config)
            if tab_config_dialog.exec_():
                updated_tab_config = tab_config_dialog.get_config()
                self.config[index] = updated_tab_config
                self.update_tabs_list()
        else:
            QtWidgets.QMessageBox.warning(self, "Edit Tab", "No tab selected.")

    def delete_tab(self):
        selected_items = self.tabs_list.selectedItems()
        if selected_items:
            index = self.tabs_list.row(selected_items[0])
            del self.config[index]
            self.update_tabs_list()
        else:
            QtWidgets.QMessageBox.warning(
                self, "Delete Tab", "No tab selected.")

    def get_config(self):
        return self.config


class TabConfigDialog(QtWidgets.QDialog):
    def __init__(self, parent, ctc100_devices, ls224_devices, ls372_devices, tab_config=None):
        super().__init__(parent)
        self.setWindowTitle("Tab Configuration")
        self.ctc100_devices = ctc100_devices
        self.ls224_devices = ls224_devices
        self.ls372_devices = ls372_devices
        self.tab_config = tab_config or {'tab_name': '', 'thermometers': []}
        self.initUI()

    def initUI(self):
        layout = QtWidgets.QVBoxLayout()

        # Tab name
        form_layout = QtWidgets.QFormLayout()
        self.tab_name_edit = QtWidgets.QLineEdit(self.tab_config['tab_name'])
        form_layout.addRow("Tab Name:", self.tab_name_edit)
        layout.addLayout(form_layout)

        # Thermometer selection
        self.thermometer_list = QtWidgets.QListWidget()
        self.thermometer_list.setSelectionMode(
            QtWidgets.QAbstractItemView.MultiSelection)
        layout.addWidget(QtWidgets.QLabel("Select Thermometers:"))
        layout.addWidget(self.thermometer_list)

        # Populate thermometer list
        self.populate_thermometer_list()

        # Pre-select existing thermometers, if editing
        selected_ids = [self.get_thermometer_id(
            t) for t in self.tab_config['thermometers']]
        for index in range(self.thermometer_list.count()):
            item = self.thermometer_list.item(index)
            thermometer_id = item.data(QtCore.Qt.UserRole)
            if thermometer_id in selected_ids:
                item.setSelected(True)

        # OK and Cancel buttons
        ok_cancel_layout = QtWidgets.QHBoxLayout()
        ok_btn = QtWidgets.QPushButton("OK")
        cancel_btn = QtWidgets.QPushButton("Cancel")
        ok_cancel_layout.addWidget(ok_btn)
        ok_cancel_layout.addWidget(cancel_btn)
        layout.addLayout(ok_cancel_layout)

        ok_btn.clicked.connect(self.accept)
        cancel_btn.clicked.connect(self.reject)

        self.setLayout(layout)

    def populate_thermometer_list(self):
        self.thermometer_list.clear()
        # Add CTC100 devices
        for i, device in enumerate(self.ctc100_devices):
            for channel in device.channels:
                thermometer_id = f"CTC100_{i}_Ch{channel}"
                item_text = f"CTC100 #{i} Channel {channel}"
                item = QtWidgets.QListWidgetItem(item_text)
                item.setData(QtCore.Qt.UserRole, thermometer_id)
                self.thermometer_list.addItem(item)

        # Add LakeShore224 devices
        for i, device in enumerate(self.ls224_devices):
            for channel in device.channels:
                thermometer_id = f"LakeShore224_{i}_Ch{channel}"
                item_text = f"LakeShore224 #{i} Channel {channel}"
                item = QtWidgets.QListWidgetItem(item_text)
                item.setData(QtCore.Qt.UserRole, thermometer_id)
                self.thermometer_list.addItem(item)

        # Add LakeShore372 devices
        for i, device in enumerate(self.ls372_devices):
            for channel in device.channels:
                thermometer_id = f"LakeShore372_{i}_Ch{channel}"
                item_text = f"LakeShore372 #{i} Channel {channel}"
                item = QtWidgets.QListWidgetItem(item_text)
                item.setData(QtCore.Qt.UserRole, thermometer_id)
                self.thermometer_list.addItem(item)

    def get_thermometer_id(self, thermo):
        device_type = thermo['device']
        device_index = thermo['device_index']
        channel = thermo['channel']
        return f"{device_type}_{device_index}_Ch{channel}"

    def get_config(self):
        tab_name = self.tab_name_edit.text()
        selected_items = self.thermometer_list.selectedItems()
        thermometers = []
        for item in selected_items:
            thermometer_id = item.data(QtCore.Qt.UserRole)
            device_type, device_index_str, channel_str = self.parse_thermometer_id(
                thermometer_id)
            thermometers.append({
                'device': device_type,
                'device_index': int(device_index_str),
                'channel': int(channel_str),
            })
        return {'tab_name': tab_name, 'thermometers': thermometers}

    def parse_thermometer_id(self, thermometer_id):
        parts = thermometer_id.split('_')
        device_type = parts[0]
        device_index = parts[1]
        channel_str = parts[2][2:]  # Remove 'Ch' prefix
        return device_type, device_index, channel_str


class TemperatureMonitorApp(QtWidgets.QMainWindow):
    def __init__(self, ctc100_devices, ls224_devices, ls372_devices):
        super().__init__()

        self.ctc100_devices = ctc100_devices    # List of CTC100 devices
        self.ls224_devices = ls224_devices      # List of LakeShore224 devices
        self.ls372_devices = ls372_devices      # List of LakeShore372 devices

        # Configuration data
        self.config = []  # This will be set via the configuration dialog

        # Data buffers and plot widgets
        self.max_data_points = 1000  # Max points to display in plots
        self.data_buffers = {}  # Key: thermometer ID, Value: list of readings
        self.plot_widgets = {}  # Key: thermometer ID, Value: plot widget

        self.initUI()
        self.start_threads()

    def initUI(self):
        self.setWindowTitle('Temperature Monitor')
        self.setGeometry(100, 100, 1000, 800)

        # Create Menu Bar
        self.createMenuBar()

        # Central Widget
        self.central_widget = QtWidgets.QWidget()
        self.setCentralWidget(self.central_widget)

        # Layout
        self.main_layout = QtWidgets.QVBoxLayout()
        self.central_widget.setLayout(self.main_layout)

        # Tab Widget
        self.tabs = QtWidgets.QTabWidget()
        self.main_layout.addWidget(self.tabs)

        # Add "Save Data" Button at the bottom
        self.save_button = QtWidgets.QPushButton("Save Data to CSV")
        self.save_button.clicked.connect(self.save_data_to_csv)
        self.main_layout.addWidget(self.save_button)

        # Initialize with default configuration
        # Optionally, you can load a saved configuration here

    def createMenuBar(self):
        # Ensure that the menu bar is created properly
        menubar = self.menuBar()

        # Force menu bar into the application window
        menubar.setNativeMenuBar(False)

        # Configuration Menu
        config_menu = menubar.addMenu('Configuration')

        # Configure Tabs Action
        configure_action = QAction('Configure Tabs...', self)
        configure_action.setShortcut('Ctrl+T')
        configure_action.triggered.connect(self.open_configuration_dialog)
        config_menu.addAction(configure_action)

        # Add Save Data Action
        save_data_action = QAction('Save Data to CSV', self)
        save_data_action.setShortcut('Ctrl+S')
        save_data_action.triggered.connect(self.save_data_to_csv)
        menubar.addAction(save_data_action)

        # You can add more menus and actions here if needed

    def open_configuration_dialog(self):
        config_dialog = ConfigurationDialog(
            self, self.ctc100_devices, self.ls224_devices, self.ls372_devices)
        config_dialog.config = self.config  # Pass current configuration
        if config_dialog.exec_():
            # Update configuration and rebuild tabs
            self.config = config_dialog.get_config()
            self.build_tabs()

    def build_tabs(self):
        # Clear existing tabs
        self.tabs.clear()
        self.data_buffers.clear()
        self.plot_widgets.clear()

        # Create tabs based on the configuration
        for tab_config in self.config:
            tab_name = tab_config['tab_name']
            thermometers = tab_config['thermometers']

            tab = QtWidgets.QWidget()
            tab_layout = QtWidgets.QVBoxLayout()
            tab.setLayout(tab_layout)
            self.tabs.addTab(tab, tab_name)

            for thermo in thermometers:
                device_type = thermo['device']
                device_index = thermo.get('device_index', 0)
                channel = thermo['channel']
                thermometer_id = f"{device_type}_{device_index}_Ch{channel}"

                # Create a plot widget for this thermometer
                plot_widget = pg.PlotWidget(
                    title=f"{device_type} #{device_index} Channel {channel}")
                plot_widget.setYRange(0, 400)  # Adjust as needed
                tab_layout.addWidget(plot_widget)

                # Initialize data buffer and store plot widget
                self.data_buffers[thermometer_id] = []
                self.plot_widgets[thermometer_id] = plot_widget

                # If applicable, add heater control (e.g., for devices with heaters)
                # Modify as needed based on your hardware setup
                # Assuming heater on channel 1
                if device_type == 'CTC100' and channel in [1]:
                    device = self.ctc100_devices[device_index]
                    heater_control_layout = QtWidgets.QHBoxLayout()
                    heater_label = QtWidgets.QLabel(f"Heater Output (%):")
                    heater_label.setFixedWidth(120)
                    heater_dial = QtWidgets.QDial()
                    heater_dial.setMinimum(0)
                    heater_dial.setMaximum(100)
                    heater_dial.setNotchesVisible(True)
                    heater_dial.setValue(0)
                    heater_dial.setWrapping(False)
                    heater_dial.valueChanged.connect(
                        self.create_heater_dial_callback(device))
                    heater_value_label = QtWidgets.QLabel("0%")
                    heater_value_label.setFixedWidth(40)
                    heater_control_layout.addWidget(heater_label)
                    heater_control_layout.addWidget(heater_dial)
                    heater_control_layout.addWidget(heater_value_label)
                    heater_control_layout.addStretch()
                    # Store the heater value label to update its text when the dial is changed
                    device.heater_value_label = heater_value_label
                    tab_layout.addLayout(heater_control_layout)

                # Similar heater control for LakeShore372, if applicable
                # Assuming heater on channel 1
                if device_type == 'LakeShore372' and channel in [1]:
                    device = self.ls372_devices[device_index]
                    heater_control_layout = QtWidgets.QHBoxLayout()
                    heater_label = QtWidgets.QLabel('Heater Output (%):')
                    heater_label.setFixedWidth(120)
                    heater_dial = QtWidgets.QDial()
                    heater_dial.setMinimum(0)
                    heater_dial.setMaximum(100)
                    heater_dial.setNotchesVisible(True)
                    heater_dial.setValue(0)
                    heater_dial.setWrapping(False)
                    heater_dial.valueChanged.connect(
                        self.create_ls372_heater_dial_callback(device))
                    heater_value_label = QtWidgets.QLabel("0%")
                    heater_value_label.setFixedWidth(40)
                    heater_control_layout.addWidget(heater_label)
                    heater_control_layout.addWidget(heater_dial)
                    heater_control_layout.addWidget(heater_value_label)
                    heater_control_layout.addStretch()
                    # Store the heater value label to update its text when the dial is changed
                    device.heater_value_label = heater_value_label
                    tab_layout.addLayout(heater_control_layout)

    def create_heater_dial_callback(self, device):
        def dial_changed(value):
            device.enableHeater()
            device.setHeaterOutput(value)
            # Update the heater value label
            device.heater_value_label.setText(f"{value}%")
        return dial_changed

    def create_ls372_heater_dial_callback(self, device):
        def dial_changed(value):
            device.set_heater_output(heater_number=1, heat_percent=value)
            # Update the heater value label
            device.heater_value_label.setText(f"{value}%")
        return dial_changed

    def start_threads(self):
        # Thread for updating temperatures
        self.update_thread = threading.Thread(target=self.update_temperatures)
        self.update_thread.daemon = True
        self.update_thread.start()

    def update_temperatures(self):
        while True:
            if not self.config:
                # No configuration set; skip reading temperatures
                time.sleep(1)
                continue

            # Acquire data for all thermometers in the configuration
            for tab_config in self.config:
                thermometers = tab_config['thermometers']
                for thermo in thermometers:
                    device_type = thermo['device']
                    device_index = thermo.get('device_index', 0)
                    channel = thermo['channel']
                    thermometer_id = f"{device_type}_{device_index}_Ch{channel}"
                    device = None

                    try:
                        if device_type == 'CTC100':
                            device = self.ctc100_devices[device_index]
                            temp = device.read(channel)
                        elif device_type == 'LakeShore224':
                            device = self.ls224_devices[device_index]
                            temp = device.get_temperature(channel)
                        elif device_type == 'LakeShore372':
                            device = self.ls372_devices[device_index]
                            temp = device.get_temperature(channel)
                        else:
                            print(f"Unknown device type: {device_type}")
                            temp = None

                        if temp is not None:
                            data_buffer = self.data_buffers[thermometer_id]
                            data_buffer.append(temp)
                            if len(data_buffer) > self.max_data_points:
                                data_buffer.pop(0)
                        else:
                            print(f"Error reading {thermometer_id}")
                    except Exception as e:
                        print(f"Error reading {thermometer_id}: {e}")

            # Update plots
            self.update_plots()

            time.sleep(1)  # Adjust the update interval as needed

    def update_plots(self):
        QtCore.QMetaObject.invokeMethod(
            self, "refresh_plots", QtCore.Qt.QueuedConnection)

    @QtCore.pyqtSlot()
    def refresh_plots(self):
        # Update all plots
        for thermometer_id, plot_widget in self.plot_widgets.items():
            data = self.data_buffers[thermometer_id]
            if data:
                x = list(range(len(data)))
                y = data
                plot_widget.clear()
                plot_widget.plot(x, y, pen='b')

    # Add the function to save data to CSV
    def save_data_to_csv(self):
        if not self.data_buffers:
            QtWidgets.QMessageBox.warning(
                self, "Save Data", "No data to save.")
            return

        # Open file dialog to choose save location
        options = QFileDialog.Options()
        filename, _ = QFileDialog.getSaveFileName(
            self, "Save Data to CSV", "", "CSV Files (*.csv);;All Files (*)", options=options)
        if filename:
            try:
                import csv
                # Organize data into a dictionary with timestamps
                max_length = max(len(data)
                                 for data in self.data_buffers.values())
                timestamps = list(range(max_length))

                # Prepare the header
                headers = ['Time (s)'] + list(self.data_buffers.keys())

                # Create data rows
                data_rows = []
                for i in range(max_length):
                    row = [timestamps[i]]
                    for thermometer_id in self.data_buffers.keys():
                        data = self.data_buffers[thermometer_id]
                        if i < len(data):
                            row.append(data[i])
                        else:
                            row.append('')
                    data_rows.append(row)

                # Write to CSV
                with open(filename, 'w', newline='') as csvfile:
                    csvwriter = csv.writer(csvfile)
                    csvwriter.writerow(headers)
                    csvwriter.writerows(data_rows)

                QtWidgets.QMessageBox.information(
                    self, "Save Data", f"Data saved successfully to {filename}")
            except Exception as e:
                QtWidgets.QMessageBox.critical(
                    self, "Save Data", f"An error occurred while saving data: {e}")
        else:
            # User canceled the file dialog
            return

# ============================ Main Execution ============================= #


def main():
    # Define the serial ports and channels for the devices
    ctc100_configs = [
        # Replace with actual port and channels
        {'port': 'COM3', 'channels': [1, 2]},
        {'port': 'COM4', 'channels': [1, 2, 3]},  # Second CTC100 device
    ]

    ls224_configs = [
        {'port': 'COM5', 'channels': [1, 2, 3, 4]},
    ]

    ls372_configs = [
        {'port': 'COM6', 'channels': [1, 2]},
    ]

    # Initialize devices
    ctc100_devices = []
    for i, config in enumerate(ctc100_configs):
        port = config['port']
        channels = config['channels']
        try:
            ctc100 = CTC100(port, channels=channels)
            print(f"Connected to CTC100 on {port} with channels {channels}")
            ctc100_devices.append(ctc100)
        except (serial.SerialException, OSError) as e:
            print(f"Could not connect to CTC100 on {port}: {e}")
            print(f"Using simulated CTC100 device at port {port}.")
            ctc100 = SimulatedCTC100(
                f"Simulated_CTC100_{i}", channels=channels)
            ctc100_devices.append(ctc100)

    ls224_devices = []
    for i, config in enumerate(ls224_configs):
        port = config['port']
        channels = config['channels']
        try:
            ls224 = LakeShore224Device(port, channels=channels)
            print(
                f"Connected to Lake Shore 224 on {port} with channels {channels}")
            ls224_devices.append(ls224)
        except Exception as e:
            print(f"Could not connect to Lake Shore 224 on {port}: {e}")
            print(f"Using simulated Lake Shore 224 device at port {port}.")
            ls224 = SimulatedLakeShore224Device(
                f"Simulated_LS224_{i}", channels=channels)
            ls224_devices.append(ls224)

    ls372_devices = []
    for i, config in enumerate(ls372_configs):
        port = config['port']
        channels = config['channels']
        try:
            ls372 = LakeShore372Device(port, channels=channels)
            print(
                f"Connected to Lake Shore 372 on {port} with channels {channels}")
            ls372_devices.append(ls372)
        except Exception as e:
            print(f"Could not connect to Lake Shore 372 on {port}: {e}")
            print(f"Using simulated Lake Shore 372 device at port {port}.")
            ls372 = SimulatedLakeShore372Device(
                f"Simulated_LS372_{i}", channels=channels)
            ls372_devices.append(ls372)

    # Start the application
    app = QtWidgets.QApplication(sys.argv)
    # Set application name (optional)
    app.setApplicationName('Temperature Monitor')
    # Set organization name (optional)
    app.setOrganizationName('YourOrganization')

    monitor_app = TemperatureMonitorApp(
        ctc100_devices, ls224_devices, ls372_devices)
    monitor_app.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()


I want you to add to this code some things: a function in each device class that allows you to list channels (both input and output), and a function that allow you to change the channels name in the interface. Also I would like dials to control all the heaters and IO channels in the devices