from KSPLIB_NSeries import _DeviceList,SPdbUSBm,_DevInformation
import os
import ctypes
from ctypes import *
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
from tkinter import filedialog
import csv
from threading import Thread
import time

try:
    from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
    import matplotlib.pyplot as plt
except ImportError:
    subprocess.check_call([sys.executable, "-m", "pip", "install", "matplotlib"])
    import matplotlib.pyplot as plt
    from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

class SDK_Net(tk.Tk):
    def __init__(self,root):
        current_dir = os.path.dirname(os.path.abspath(__file__))
        dll_path = os.path.join(current_dir, 'SPdbEthm.dll')
        self.m_KSPLIB_NSeries = SPdbUSBm(dll_path)
        self.device_list = _DeviceList()
        self.deviceInfo = _DevInformation() 
        self.selDev = -1
        self.root = root
        self.sTotChnl = 0
        self.root.title("Device Scanner")
        self.root.geometry("600x550")
        

        # Device Info Table
        self.device_table = ttk.Treeview(self.root, columns=("No", "Type", "In Use", "Device", "Serial", "IP Address", "Port"), show="headings")
        self.device_table.heading("No", text="No")
        self.device_table.heading("Type", text="Type")
        self.device_table.heading("In Use", text="In Use")
        self.device_table.heading("Device", text="Device")
        self.device_table.heading("Serial", text="Serial")
        self.device_table.heading("IP Address", text="IP Address")
        self.device_table.heading("Port", text="Port")
        self.device_table.column("No", width=30)
        self.device_table.column("Type", width=50)
        self.device_table.column("In Use", width=50)
        self.device_table.column("Device", width=100)
        self.device_table.column("Serial", width=100)
        self.device_table.column("IP Address", width=100)
        self.device_table.column("Port", width=50)
        self.device_table.place(x=10, y=10, width=580, height=150)

        # NetworkID and HostID Range
        netwerk_frame = tk.LabelFrame(self.root)
        netwerk_frame.place(x=10, y=165, width=270, height=95)
        tk.Label(self.root, text="NetworkID").place(x=15, y=170)
        self.network_id = tk.Entry(self.root, width=10)
        self.network_id.place(x=15, y=190)
        self.network_id.insert(0, "192.168.1")

        tk.Label(self.root, text="HostID Range").place(x=95, y=170)
        self.host_id_start = tk.Entry(self.root, width=5)
        self.host_id_start.place(x=95, y=190)
        self.host_id_start.insert(0, "1")
        tk.Label(self.root, text="-").place(x=145, y=190)
        self.host_id_end = tk.Entry(self.root, width=5)
        self.host_id_end.place(x=163, y=190)
        self.host_id_end.insert(0, "255")

        # Scan Buttons
        self.eth_scan_var = tk.BooleanVar()
        tk.Checkbutton(self.root, text="ETH IP Range Scan", variable=self.eth_scan_var).place(x=15, y=220)
        tk.Button(self.root, text="Device Scan", width=10,command = self.scan_device).place(x=170, y=220)

        # Control Buttons
        tk.Button(self.root, text="Connect", width=20, command = self.connect_click).place(x=290, y=165)
        tk.Button(self.root, text="Close", width=10, command = self.close).place(x=450, y=165)


        # Memory Control
        memory_frame = tk.LabelFrame(self.root, text = "Memory Control")
        memory_frame.place(x=290, y=200, width=270, height=70)
        self.wavebtn = tk.Button(self.root, text="Wavelength\nCalibration", width=12, height=2,command =self.opne_wave)
        self.wavebtn.place(x=310, y=218)
        self.burstbtn = tk.Button(self.root, text="Burst Mode", width=12,height=2,command= self.opne_burst)
        self.burstbtn.place(x=420, y=218)

        # Trigger Mode Options
        trigger_frame = tk.LabelFrame(self.root, text="Trigger Mode")
        trigger_frame.place(x=10, y=280, width=180, height=140)

        self.trigger_option = tk.IntVar()
        self.rb_free_run_next = tk.Radiobutton(trigger_frame, text="Free Run Next", variable=self.trigger_option, value=2)
        self.rb_free_run_next.place(x=10, y=10)

        self.rb_sw_trigger = tk.Radiobutton(trigger_frame, text="S/W Trigger", variable=self.trigger_option, value=3)
        self.rb_sw_trigger.place(x=10, y=30)

        self.rb_external_trigger = tk.Radiobutton(trigger_frame, text="External Trigger", variable=self.trigger_option, value=4)
        self.rb_external_trigger.place(x=10, y=50)

        self.rb_free_run_previous = tk.Radiobutton(trigger_frame, text="Free Run Previous", variable=self.trigger_option, value=1)
        self.rb_free_run_previous.place(x=10, y=70)

        self.rb_sync_trigger = tk.Radiobutton(trigger_frame, text="Synchronous Trigger", variable=self.trigger_option, value=5, state="disabled")
        self.rb_sync_trigger.place(x=10, y=90)

        self.trigger_option.set(1)

        # Data Acquisition Options
        acquisition_frame = tk.LabelFrame(self.root, text="Data Acquisition")
        acquisition_frame.place(x=200, y=280, width=280, height=130)

        self.lbl_integration_time = tk.Label(acquisition_frame, text="Integration Time:")
        self.lbl_integration_time.place(x=10, y=10)

        self._last_valid_value = 35
        self.integration_time_var = tk.IntVar(value=self._last_valid_value)
        self.integration_time_var.trace("w", self._validate)
        self.integration_time = tk.Spinbox(acquisition_frame, width=15, to=100000,textvariable=self.integration_time_var)
        self.integration_time.place(x=110, y=10)

        self.lbl_time_average = tk.Label(acquisition_frame, text="Time Average:")
        self.lbl_time_average.place(x=10, y=40)
        
        self._last_timeavg_value = 1
        self.time_average_var = tk.IntVar(value=self._last_timeavg_value)
        self.time_average_var.trace("w", self._validate_avg)
        self.time_average = tk.Spinbox(acquisition_frame, width=15, to=100000)
        self.time_average.place(x=110, y=40)
        self.time_average.insert(0, "1")

        self.auto_dark_var = tk.BooleanVar()
        self.chk_auto_dark = tk.Checkbutton(acquisition_frame, text="Auto Dark", variable=self.auto_dark_var,command=self.chk_autodark)
        self.chk_auto_dark.place(x=10, y=70)

        self.btn_apply = tk.Button(acquisition_frame, text="Apply", width=10,command=self.btn_apply)
        self.btn_apply.place(x=110, y=70)

        # Wavelength Table Selection
        wavelength_frame = tk.LabelFrame(self.root, text="Select Wavelength Table")
        wavelength_frame.place(x=10, y=420, width=470, height=95)

        self.selected_option = tk.IntVar()
        self.rb_user_memory_coefficient = tk.Radiobutton(wavelength_frame, text="Coefficient stored in user memory", value=1, variable=self.selected_option, command= self.sel_wl_table_radio_check_changed)
        self.rb_user_memory_coefficient.place(x=10, y=10)

        self.rb_factory_default = tk.Radiobutton(wavelength_frame, text="Factory Default", value=2, variable=self.selected_option, command= self.sel_wl_table_radio_check_changed)
        self.rb_factory_default.place(x=240, y=10)

        self.rb_user_memory_cal_point = tk.Radiobutton(wavelength_frame, text="Cal Point stored in user memory", value=3, variable=self.selected_option, command= self.sel_wl_table_radio_check_changed)
        self.rb_user_memory_cal_point.place(x=10, y=30)

        self.selected_option.set(2)

        # Get Data Buttons
        self.btn_get_data_text = tk.Button(self.root, text="Get Data\n(Text)", width=12, height=2,command = self.get_data_set)
        self.btn_get_data_text.place(x=490, y=400)

        self.btn_get_data_graph = tk.Button(self.root, text="Get Data\n(Graph)", width=12, height=2,command = self.open_graph)
        self.btn_get_data_graph.place(x=490, y=460)

        self.buttonEnable("disabled")
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)

    def _validate(self,*args):
        try:
            self.integration_time_var.get()
        except:
            self.integration_time_var.set(self._last_valid_value)
        value = self.integration_time_var.get()
        if value < 0 or value > 100000:
            # If the value is invalid, it reverts if it is the last one.
            self.integration_time_var.set(self._last_valid_value)
        else:
            self._last_valid_value = value  # Save the valid value.

    def _validate_avg(self,*args):
        try:
            self.time_average_var.get()
        except:
            self.time_average_var.set(self._last_timeavg_value)
        value = self.time_average_var.get()
        if value < 0 or value > 100000:
            # Revert to the last valid value if invalid
            self.time_average_var.set(self._last_timeavg_value)
        else:
            self._last_timeavg_value = value  # Save the valid value.

    def scan_device(self):
        for i in range(len(self.device_table.get_children())):
            values = self.device_table.item(self.device_table.get_children()[i])['values']
            in_use = values[2] == 'True'
            if in_use:
                channel = self.deviceInfo[i].sChannel
                self.m_KSPLIB_NSeries.NDevClose(channel)
    
        # table reset
        for item in self.device_table.get_children():
            self.device_table.delete(item)

        # device search
        sRtn = -1
        if not self.eth_scan_var.get():
            sRtn = self.m_KSPLIB_NSeries.NScanDevice(1)  # SP_SCAN_ALLDEVICE MODE

            if sRtn < 0:
                error_message = f"There are no matching devices.\nError String: {self.m_KSPLIB_NSeries.NGetErrorString(sRtn)}"
                tk.messagebox.showerror("ERROR", error_message)
        else:
            # Get the range of Network ID and Host ID.
            pcNetworkID = self.network_id.get()  # Get Network ID Text.
            start_host_id = int(self.host_id_start.get())
            end_host_id = int(self.host_id_end.get())

            # Perform IP range scan.
            sRtn = self.m_KSPLIB_NSeries.NScanDevice_IP(pcNetworkID, start_host_id, end_host_id)
            if sRtn < 0:
                error_message = f"There are no matching devices.\nError String: {self.m_KSPLIB_NSeries.NGetErrorString(sRtn)}"
                tk.messagebox.showerror("ERROR", error_message)
    
        # Initialize and retrieve device information list.
        self.sTotChnl = sRtn
        self.device_list = (_DeviceList * self.sTotChnl)() 
        self.deviceInfo = [_DevInformation()  for _ in range(self.sTotChnl)]

        sRtn = self.m_KSPLIB_NSeries.NGetDeviceList(self.device_list)

        for i in range(self.sTotChnl):
            interface_type = "TCP/IP" if self.device_list[i].sInterfaceType == 1 else "USB"  # SP_INTERFACE_ETHERNET = 1
            self.device_table.insert("", "end", values=(
                i + 1, 
                interface_type, 
                False, 
                self.device_list[i].strModel.decode('utf-8'),  # Decode to regular string
                self.device_list[i].strSerial.decode('utf-8'),  # Decode to regular string
                self.device_list[i].strIPAddr.decode('utf-8'),  # Decode to regular string
                self.device_list[i].strCOM.decode('utf-8')       # Decode to regular string
            ))
    def buttonEnable(self,state = "normal"):
        # Trigger Mode Radio Buttons
        self.rb_free_run_next.config(state=state)
        self.rb_sw_trigger.config(state=state)
        self.rb_external_trigger.config(state=state)
        self.rb_free_run_previous.config(state=state)
        self.wavebtn.config(state=state)
        self.burstbtn.config(state=state)
        # Data Acquisition Spinboxes and Checkbutton
        self.integration_time.config(state=state)
        self.time_average.config(state=state)
        self.chk_auto_dark.config(state=state)
        self.btn_apply.config(state=state)
    
        # Wavelength Table Radio Buttons
        self.rb_user_memory_coefficient.config(state=state)
        self.rb_factory_default.config(state=state)
        self.rb_user_memory_cal_point.config(state=state)
    
        # Get Data Buttons
        self.btn_get_data_text.config(state=state)
        self.btn_get_data_graph.config(state=state)
        pass

    def deconnect(self):
        self.m_KSPLIB_NSeries.NDevClose(self.deviceInfo[self.selDev].sChannel)

    def connect_click(self):
        sRtn = -1
        selected_item = self.device_table.selection()  # Get the ID of the selected item.
        if selected_item:
            self.selDev = self.device_table.index(selected_item)  # Get the index of the selected item.
        else:
            return
        if self.device_list[self.selDev].sInterfaceType == 1:
            sRtn = self.m_KSPLIB_NSeries.NConnect(self.device_list[self.selDev].sInterfaceType,self.device_list[self.selDev].strIPAddr)
        elif self.device_list[self.selDev].sInterfaceType == 0:
            sRtn = self.m_KSPLIB_NSeries.NConnect(self.device_list[self.selDev].sInterfaceType,self.device_list[self.selDev].strCOM)

        if sRtn < 0:
            error_message = f"There are no matching devices.\nError String: {self.m_KSPLIB_NSeries.NGetErrorString(sRtn)}"
            tk.messagebox.showerror("ERROR", error_message)
            return
        sRtn = self.m_KSPLIB_NSeries.NGetDevParam(self.deviceInfo[self.selDev], sRtn);
        if sRtn < 0:
            error_message = f"Error Function : spNGetDevParam().\nError String : {self.m_KSPLIB_NSeries.NGetErrorString(sRtn)}"
            tk.messagebox.showerror("ERROR", error_message)
            return

        self.buttonEnable()
        if self.deviceInfo[self.selDev].strModel.decode() in ["SM303NP", "SM303N"]:
            self.rb_sync_trigger.config(state="normal")

        if self.deviceInfo[self.selDev].sCCDType == 0:
            new_value = self.deviceInfo[self.selDev].iInttime
            self.integration_time_var.set(new_value)
        else:
            self.integration_time_var.set(self.deviceInfo[self.selDev].iInttime / 1000.0)
        new_value = self.deviceInfo[self.selDev].iTimeavg
        self.time_average.delete(0, "end")
        self.time_average.insert(0, new_value)

        # Iterate over all items and update the value of the third column.
        for item in self.device_table.get_children():
            # Set the value of the third column to True for the selected item.
            if item == selected_item[0]:
                self.device_table.set(item, column=2, value="True")
            # Set the value of the third column to False for the selected item.
            else:
                self.device_table.set(item, column=2, value="False")

        if self.selected_option.get() ==  2:
            sRtn = self.m_KSPLIB_NSeries.NGetWLTable(self.deviceInfo[self.selDev].dWLTable, self.deviceInfo[self.selDev].sChannel)
            if(sRtn < 0):
                error_message = f"Error Function : spNGetWLTable().\nError String : {self.m_KSPLIB_NSeries.NGetErrorString(sRtn)}"
                tk.messagebox.showerror("ERROR", error_message)
        elif self.selected_option.get() == 1:
            sRtn = self.m_KSPLIB_NSeries.NGetWLTable_User(self.deviceInfo[self.selDev].dWLTable, 0,self.deviceInfo[self.selDev].sChannel)
            if(sRtn < 0):
                error_message = f"Error Function : spNGetWLTable().\nError String : {self.m_KSPLIB_NSeries.NGetErrorString(sRtn)}"
                tk.messagebox.showerror("ERROR", error_message)
        elif self.selected_option.get() == 3:
            sRtn = self.m_KSPLIB_NSeries.NGetWLTable_User(self.deviceInfo[self.selDev].dWLTable, 1,self.deviceInfo[self.selDev].sChannel)
            if(sRtn < 0):
                error_message = f"Error Function : spNGetWLTable().\nError String : {self.m_KSPLIB_NSeries.NGetErrorString(sRtn)}"
                tk.messagebox.showerror("ERROR", error_message)

    def on_closing(self):
        # Iterate through all channels, check the conditions, and close the device.
        for i in range(self.sTotChnl):
            if self.device_table.set(self.device_table.get_children()[i], column=2) == 'True':
                self.m_KSPLIB_NSeries.NDevClose(self.deviceInfo[i].sChannel)

        self.root.destroy()

    def get_data_set(self):
        # Initialize the pixel data array.
        data_array = (ctypes.c_int * self.deviceInfo[self.selDev].iTotPixel)()

        # Set up the file save dialog.
        file_path = filedialog.asksaveasfilename(
            defaultextension=".txt",
            filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")],
            initialfile="SDK_Data_1.txt",
            title="Please specify the location to save"
        )

        # Check if the trigger mode is external trigger.
        if self.deviceInfo[self.selDev].sTrgMode == 4:
            # Attempt to read data
            s_rtn = -99
            while s_rtn == -99:
                s_rtn = self.m_KSPLIB_NSeries.NReadDataEx(data_array, self.deviceInfo[self.selDev].sChannel)
        else:
            s_rtn = self.m_KSPLIB_NSeries.NReadDataEx(data_array, self.deviceInfo[self.selDev].sChannel)

        # file selection save
        if file_path:
            with open(file_path, 'w') as file:
                # header
                stream_txt = "Index\tWavelength(nm)\tIntensity\n"
            
                # pixel data add
                for i in range(self.deviceInfo[self.selDev].iRealPixel):
                    wavelength = self.deviceInfo[self.selDev].dWLTable[i]
                    intensity = data_array[self.deviceInfo[self.selDev].iDummyPixel + i]
                    stream_txt += f"{i}\t{wavelength:.2f}\t{intensity:.2f}\n"
            
                # file writh
                file.write(stream_txt)

    def chk_autodark(self):
        sRtn = -1
        if self.auto_dark_var:
            sRtn = self.m_KSPLIB_NSeries.NAutoDark(1,self.deviceInfo[self.selDev].sChannel)
        else:
            sRtn = self.m_KSPLIB_NSeries.NAutoDark(0,self.deviceInfo[self.selDev].sChannel)
        if sRtn < 0:
            error_message = self.m_KSPLIB_NSeries.NGetErrorString(sRtn)
            messagebox.showerror("ERROR", f"Error Function: NAutoDark().\nError String: {error_message}")
            return

    def sel_wl_table_radio_check_changed(self):
        # selection raidio btn
        selected_value = self.selected_option.get()
        sRtn = 0
        
        try:
            if selected_value == 1:  # Coefficient
                sRtn = self.m_KSPLIB_NSeries.NGetWLTable_User(self.deviceInfo[self.selDev].dWLTable, 0, self.deviceInfo[self.selDev].sChannel)
            elif selected_value == 3:  # Point
                sRtn = self.m_KSPLIB_NSeries.NGetWLTable_User(self.deviceInfo[self.selDev].dWLTable, 1, self.deviceInfo[self.selDev].sChannel)
            elif selected_value == 2:  # Factory
                sRtn = self.m_KSPLIB_NSeries.NGetWLTable(self.deviceInfo[self.selDev].dWLTable, self.deviceInfo[self.selDev].sChannel)
            
            # Error message
            if sRtn < 0:
                error_message = self.m_KSPLIB_NSeries.NGetErrorString(sRtn)
                messagebox.showerror("ERROR", f"Error Function: NGetWLTable().\nError String: {error_message}")
        
        except Exception as e:
            messagebox.showerror("ERROR", f"An error occurred: {e}")

    def btn_apply(self):
        if self.selDev >= 0:
            self.deviceInfo[self.selDev].sTrgMode = self.trigger_option.get()

        if self.deviceInfo[self.selDev].sCCDType == 0:
            sRtn = self.m_KSPLIB_NSeries.NSetIntTime(self.integration_time_var.get(),self.deviceInfo[self.selDev].sChannel)
        else:
            sRtn = self.m_KSPLIB_NSeries.NSetDBLIntTime(self.integration_time_var.get(),self.deviceInfo[self.selDev].sChannel)

        if sRtn < 0:
            error_message = self.m_KSPLIB_NSeries.NGetErrorString(sRtn)
            messagebox.showerror("ERROR", f"Error Function: spNSetIntTime().\nError String: {error_message}")
            return
        sRtn = self.m_KSPLIB_NSeries.NSetTimeAvg(self.time_average_var.get(),self.deviceInfo[self.selDev].sChannel)
        if sRtn < 0:
            error_message = self.m_KSPLIB_NSeries.NGetErrorString(sRtn)
            messagebox.showerror("ERROR", f"Error Function: spNSetTimeAvg().\nError String: {error_message}")
            return
        sRtn = self.m_KSPLIB_NSeries.NSetTrgMode(self.deviceInfo[self.selDev].sTrgMode,self.deviceInfo[self.selDev].sChannel)
        if sRtn < 0:
            error_message = self.m_KSPLIB_NSeries.NGetErrorString(sRtn)
            messagebox.showerror("ERROR", f"Error Function: spNSetTrgMode().\nError String: {error_message}")
            return
        if self.deviceInfo[self.selDev].sCCDType == 0:
            self.deviceInfo[self.selDev].iInttime = self.integration_time_var.get()
        else:
            self.deviceInfo[self.selDev].iInttime = self.integration_time_var.get() * 1000
        self.deviceInfo[self.selDev].iTimeavg = self.time_average_var.get()

    def close(self):
        self.m_KSPLIB_NSeries.NDevClose(self.deviceInfo[self.selDev].sChannel)

    def opne_wave(self):
        Calwavelength(self.root,self)

    def opne_burst(self):
        BurstMode(self.root,self)

    def open_graph(self):
        Graph(self.root,self)




class Graph(tk.Toplevel):
    def __init__(self,root,parent):
        if isinstance(parent, tk.Tk):
            super().__init__(root)
        else:
            raise ValueError("Parent must be an instance of tk.Tk")

        self.updateGraph = False
        self.parent = parent
        self.selDev = self.parent.selDev
        self.sSelChannel = self.parent.deviceInfo[self.selDev].sChannel
        self.m_KSPLIB_NSeries = self.parent.m_KSPLIB_NSeries

        self.title("Measurement Application")
        self.geometry("650x440")  # Adjust window size as needed

        graph_frame = tk.Frame(self, bg="white")
        graph_frame.place(x=10, y=10, width=600, height=370)

        fig, self.ax = plt.subplots(figsize=(6, 3.7))

        canvas = FigureCanvasTkAgg(fig, master=graph_frame)
        canvas.draw()
        canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

        self.start_btn = tk.Button(self, text = "start", command = self.start_graph)
        self.start_btn.place(x = 10, y= 400, width = 60)

        self.stop_btn = tk.Button(self, text = "stop", command = self.stop_graph)
        self.stop_btn.place(x = 90, y= 400, width = 60)

    def start_graph(self):
        # updateGraph state config and thread start
        if not self.updateGraph:
            self.updateGraph = True
            self.graph_thread = Thread(target=self.run_graph_loop)
            self.graph_thread.start()

    def run_graph_loop(self):
        iDataArray = (c_int * self.parent.deviceInfo[self.selDev].iTotPixel)()
        
        while self.updateGraph:
            if self.parent.deviceInfo[self.selDev].sTrgMode == 4:
                while True:
                    sRtn = self.m_KSPLIB_NSeries.NReadDataEx(iDataArray, self.sSelChannel)
                    if sRtn != -99:
                        break
            else:
                sRtn = self.m_KSPLIB_NSeries.NReadDataEx(iDataArray, self.sSelChannel)
                if sRtn < 0:
                    error_message = f"Error Function: NReadDataEx().\nError String: {self.m_KSPLIB_NSeries.NGetErrorString(sRtn)}"
                    messagebox.showerror("ERROR", error_message)
                    break
            
            # Use after to execute graph_draw on the main thread.
            self.after(30, self.graph_draw, iDataArray)
            time.sleep(0.01)

    def graph_draw(self, iDataArray):
        # Clear the current graph plot
        self.ax.clear()

        # Retrieve real pixel, wavelength table, and dummy pixel values from the device info
        real_pixel = self.parent.deviceInfo[self.parent.selDev].iRealPixel
        wl_table = self.parent.deviceInfo[self.parent.selDev].dWLTable
        dummy_pixel = self.parent.deviceInfo[self.parent.selDev].iDummyPixel

        # Create wavelength and intensity arrays
        wavelengths = wl_table[:real_pixel]
        intensities = [iDataArray[i + dummy_pixel] for i in range(real_pixel)]

        # Plot the graph with wavelengths on the x-axis and intensities on the y-axis as a solid blue line
        self.ax.plot(wavelengths, intensities, 'b-')

        # Refresh the graph display
        self.ax.figure.canvas.draw()

    def stop_graph(self):
        # Set updateGraph to False to exit the graphing loop
        self.updateGraph = False

        # Wait for the graph thread to finish if it is still running
        if self.graph_thread.is_alive():
            self.graph_thread.join(timeout=1)

        



class Calwavelength(tk.Toplevel):
    def __init__(self, root, parent):
        # Check that parent is an instance of tk.Tk and initialize the Toplevel window
        if isinstance(parent, tk.Tk):
            super().__init__(root)
        else:
            raise ValueError("Parent must be an instance of tk.Tk")
        
        # Set parent reference and initialize device-related variables
        self.parent = parent
        self.selDev = self.parent.selDev
        self.dGetWLTable = (c_double * self.parent.deviceInfo[self.selDev].iRealPixel)(*([0.0] * self.parent.deviceInfo[self.selDev].iRealPixel))
        self.sSelChannel = self.parent.deviceInfo[self.selDev].sChannel
        self.m_KSPLIB_NSeries = self.parent.m_KSPLIB_NSeries

        # Configure window title and size
        self.title("Calibration UI")
        self.geometry("700x450")

        # Frame for displaying current calibration memory data
        self.current_calibration_memory_frame = tk.LabelFrame(self, text="Current Calibration Memory")
        self.current_calibration_memory_frame.place(x=20, y=20, width=400, height=200)

        # Coefficient table to display calibration coefficients
        self.coefficient_tree = ttk.Treeview(self.current_calibration_memory_frame, columns=("Order", "Coefficient"), show="headings")
        self.coefficient_tree.heading("Order", text="Order")
        self.coefficient_tree.heading("Coefficient", text="Coefficient")
        self.coefficient_tree.column("Order", width=50)
        self.coefficient_tree.column("Coefficient", width=125)
        self.coefficient_tree.place(x=10, y=20, width=180, height=160)

        # Calibration points table to display pixel and wavelength information
        self.calibration_point_tree = ttk.Treeview(self.current_calibration_memory_frame, columns=("Pixel", "Wavelength(nm)"), show="headings")
        self.calibration_point_tree.heading("Pixel", text="Pixel")
        self.calibration_point_tree.heading("Wavelength(nm)", text="Wavelength(nm)")
        self.calibration_point_tree.column("Pixel", width=50)
        self.calibration_point_tree.column("Wavelength(nm)", width=125)
        self.calibration_point_tree.place(x=210, y=20, width=180, height=160)

        # Frame for retrieving wavelength tables
        self.get_wavelength_table_frame = tk.LabelFrame(self, text="Get Wavelength Table")
        self.get_wavelength_table_frame.place(x=420, y=20, width=200, height=400)

        # Wavelength table to display pixel and wavelength values
        self.wavelength_table_tree = ttk.Treeview(self.get_wavelength_table_frame, columns=("Pixel", "Wavelength(nm)"), show="headings")
        self.wavelength_table_tree.heading("Pixel", text="Pixel")
        self.wavelength_table_tree.heading("Wavelength(nm)", text="Wavelength(nm)")
        self.wavelength_table_tree.column("Pixel", width=50)
        self.wavelength_table_tree.column("Wavelength(nm)", width=125)
        self.wavelength_table_tree.place(x=10, y=30, width=180, height=300)

        # Dropdown to select the wavelength table type
        self.combobox = ttk.Combobox(self.get_wavelength_table_frame, values=[
            "Coefficient Memory WL Table",
            "Cal Point Memory WL Table"
        ], state="readonly")
        self.combobox.place(x=10, y=5, width=180)
        self.combobox.current(0)  # Select the first item by default
        self.combobox.bind("<<ComboboxSelected>>", self.combo_cell_wl_table)

        # Button to export wavelength table data to CSV
        self.export_button = tk.Button(self.get_wavelength_table_frame, text="Export .csv", command=self.export_wl_table)
        self.export_button.place(x=60, y=350)

        

        # Frame for writing memory data
        self.write_memory_frame = tk.LabelFrame(self, text="Write Memory")
        self.write_memory_frame.place(x=10, y=220, width=350, height=220)

        # Notebook widget to organize tabs for coefficient and calibration point data
        self.notebook = ttk.Notebook(self.write_memory_frame)
        self.notebook.place(x=0, y=0, width=330, height=180)

        # Separate frames for coefficient and calibration point data within the notebook
        self.coef_frame = ttk.Frame(self.notebook)
        self.cal_point_frame = ttk.Frame(self.notebook)

        # Adding frames to the notebook with appropriate labels
        self.notebook.add(self.coef_frame, text="Coefficient")
        self.notebook.add(self.cal_point_frame, text="Cal Point")

        # Buttons for writing and reading memory in the coefficient tab
        self.write_memory_button = tk.Button(self.coef_frame, text="Write Memory", command=self.write_memcoeff)
        self.write_memory_button.place(x=20, y=20)
        self.read_memory_button = tk.Button(self.coef_frame, text="Read Memory", command=self.read_mem_coeff)
        self.read_memory_button.place(x=20, y=60)

        # Coefficient table to display and edit data, organized with columns for order and coefficient values
        self.write_coefficient_tree = ttk.Treeview(self.coef_frame, columns=("Order", "Coefficient"), show="headings")
        self.write_coefficient_tree.heading("Order", text="Order")
        self.write_coefficient_tree.heading("Coefficient", text="Coefficient")
        self.write_coefficient_tree.column("Order", width=50)
        self.write_coefficient_tree.column("Coefficient", width=145)
        self.write_coefficient_tree.place(x=120, y=20, width=200, height=140)
        self.write_coefficient_tree.insert("", "end", values=("", f""))

        # Hidden Entry widget for in-place editing of the coefficient data in the Treeview
        self.editing_entry = tk.Entry(self.write_coefficient_tree)
        self.editing_entry.place(x=0, y=0, width=0, height=0)
        self.editing_entry.bind("<Return>", lambda event: self.save_edit(self.write_coefficient_tree, self.editing_entry, event))
        self.editing_entry.bind("<FocusOut>", lambda event: self.save_edit(self.write_calpoint_tree, self.editing_entry, event))

        # Binding double-click event on Treeview cells to start editing
        self.write_coefficient_tree.bind("<Double-1>", lambda event: self.start_edit(self.write_coefficient_tree, self.editing_entry, event))

        # Buttons for writing and reading memory in the calibration point tab
        self.write_memory_button2 = tk.Button(self.cal_point_frame, text="Write Memory", command=self.write_mem_point)
        self.write_memory_button2.place(x=20, y=20)
        self.read_memory_button2 = tk.Button(self.cal_point_frame, text="Read Memory", command=self.read_mem_point)
        self.read_memory_button2.place(x=20, y=60)

        # Calibration point table to display and edit data, organized with columns for pixel and wavelength values
        self.write_calpoint_tree = ttk.Treeview(self.cal_point_frame, columns=("Pixel", "Wavelength"), show="headings")
        self.write_calpoint_tree.heading("Pixel", text="Pixel")
        self.write_calpoint_tree.heading("Wavelength", text="Wavelength(nm)")
        self.write_calpoint_tree.column("Pixel", width=50)
        self.write_calpoint_tree.column("Wavelength", width=145)
        self.write_calpoint_tree.place(x=120, y=20, width=200, height=140)
        self.write_calpoint_tree.insert("", "end", values=("", f""))

        # Hidden Entry widget for in-place editing of calibration point data in the Treeview
        self.editing_entry2 = tk.Entry(self.write_calpoint_tree)
        self.editing_entry2.place(x=0, y=0, width=0, height=0)
        self.editing_entry2.bind("<Return>", lambda event: self.save_edit(self.write_calpoint_tree, self.editing_entry2, event))
        self.editing_entry2.bind("<FocusOut>", lambda event: self.save_edit(self.write_calpoint_tree, self.editing_entry2, event))

        # Binding double-click event on Treeview cells to start editing
        self.write_calpoint_tree.bind("<Double-1>", lambda event: self.start_edit(self.write_calpoint_tree, self.editing_entry2, event))

        # Call function to display memory data initially
        self.memdatadisplay()

    def memdatadisplay(self):
        sRtn = 0
        dcoef = (c_double * 30)(*([0.0] * 30))  # Initialize as c_double array
        sOrder = 0

        # Execute the DLL function NReadWLCalCoeff_User
        sRtn = self.m_KSPLIB_NSeries.NReadWLCalCoeff_User(dcoef, sOrder,self.sSelChannel)
        if sRtn != -133:
            # Initialize Table
            for item in self.coefficient_tree.get_children():
                self.coefficient_tree.delete(item)
            for i in range(sOrder):
                self.coefficient_tree.insert("", "end", values=(
                    i,
                    dcoef[i].decode('utf-8')
                ))
        else:
            return

        dPixel = (c_double * 30)(*([0.0] * 30))
        dWavelength = (c_double * 30)(*([0.0] * 30))
        pointNum = 0

        # Execute the DLL function NReadWLCalPoint_User
        sRtn = self.m_KSPLIB_NSeries.NReadWLCalPoint_User(dPixel,dWavelength,pointNum,self.sSelChannel)

        if sRtn != -133:
            # Initialize Table
            for item in self.calibration_point_tree.get_children():
                self.calibration_point_tree.delete(item)
            for i in range(sOrder):
                self.calibration_point_tree.insert("", "end", values=(
                    i,
                    dcoef[i].decode('utf-8')
                ))
        else:
            return

        # CalPoint or Coefficient select
        if self.combobox.current() == 0:
            for i in range(self.parent.deviceInfo[self.selDev].iRealPixel):
                # Execute the DLL function NPolyCalc
                self.m_KSPLIB_NSeries.NPolyCalc(dcoef,sOrder,i+1,self.dGetWLTable[i])
        else:
            order = 0
            dTempCoef = (c_double * 30)(*([0.0] * 30))
            if pointNum == 2:
                order = 1
            elif pointNum == 3:
                order = 2
            else:
                order = 3
            # Execute the DLL function NPolyFit
            self.m_KSPLIB_NSeries.NPolyFit(dPixel,dWavelength,pointNum,dTempCoef,order)
            for i in range(self.parent.deviceInfo[self.selDev].iRealPixel):
                # Execute the DLL function NPolyCalc
                self.m_KSPLIB_NSeries.NPolyCalc(dTempCoef,order,i+1,self.dGetWLTable[i])

        for item in self.wavelength_table_tree.get_children():
            self.wavelength_table_tree.delete(item)

        for i in range(self.parent.deviceInfo[self.selDev].iRealPixel):
            self.wavelength_table_tree.insert("","end",value=(i+1,self.dGetWLTable[i]))

        
    # write coefficient
    def write_memcoeff(self):
        dCoef = (c_double * 30)(*([0.0] * 30)) 
        sOrder = 0

        for i, row in enumerate(self.write_coefficient_tree.get_children()):
            try:
                dTempCoef = float(self.write_coefficient_tree.item(row, 'values')[1])
                iTempOrder = int(self.write_coefficient_tree.item(row, 'values')[0])
            except ValueError as ex:
                break

            dCoef[i] = dTempCoef
            sOrder = i

            if sOrder >= 30:
                break

        if sOrder != 0:
            # Execute the DLL function NWriteWLCalCoeff_User
            sRtn = self.m_KSPLIB_NSeries.NWriteWLCalCoeff_User(dCoef, sOrder, self.sSelChannel)
            if sRtn < 0:
                error_message = f"Error Function: NWriteWLCalCoeff_User().\nError String: {self.m_KSPLIB_NSeries.NGetErrorString(sRtn)}"
                messagebox.showerror("ERROR", error_message)
                return

            self.memdatadisplay()

    # read coefficient
    def read_mem_coeff(self):
        dCoef = (c_double * 10)(*([0.0] * 10))
        sOrder = 0

        # Execute the DLL function NReadWLCalCoeff_User
        sRtn = self.m_KSPLIB_NSeries.NReadWLCalCoeff_User(dCoef,sOrder,self.sSelChannel)
    
        if sRtn == -133:
            messagebox.showerror("ERROR", "Error Function: NReadWLCalCoeff_User().\nError String: Empty EEPROM.")
            self.write_coefficient_tree.delete(*self.write_coefficient_tree.get_children())
            return

        self.write_coefficient_tree.delete(*self.write_coefficient_tree.get_children())

        for i in range(sOrder + 1):
            self.write_coefficient_tree.insert("", "end", values=(str(i), str(dCoef[i])))

    # write point
    def write_mem_point(self):
        dWavelength = (c_double * 30)(*([0.0] * 30))
        dPixel = (c_double * 30)(*([0.0] * 30))
        sPointNum = 0

        for i, row in enumerate(self.write_calpoint_tree.get_children()):
            try:
                dTempPixel = float(self.write_calpoint_tree.item(row, 'values')[0])
                dTempWL = float(self.write_calpoint_tree.item(row, 'values')[1])
            except ValueError:
                break

            dPixel[i] = dTempPixel
            dWavelength[i] = dTempWL
            sPointNum = i + 1

            if sPointNum >= 30:
                break

        if sPointNum != 0:
            # Execute the DLL function NWriteWLCalPoint_User
            sRtn = self.m_KSPLIB_NSeries.NWriteWLCalPoint_User(dPixel, dWavelength, sPointNum, self.sSelChannel)
            if sRtn < 0:
                error_message = f"Error Function: NWriteWLCalPoint_User().\nError String: {self.m_KSPLIB_NSeries.NGetErrorString(sRtn)}"
                messagebox.showerror("ERROR", error_message)
                return

            self.memdatadisplay()

    # read point
    def read_mem_point(self):
        dWavelength = (c_double * 30)(*([0.0] * 30))
        dPixel = (c_double * 30)(*([0.0] * 30))
        sPointNum = 0

        # Execute the DLL function NReadWLCalPoint_User
        sRtn = self.m_KSPLIB_NSeries.NReadWLCalPoint_User(dPixel,dWavelength ,sPointNum ,self.sSelChannel)
    
        if sRtn == -133:
            messagebox.showerror("ERROR", "Error Function: NReadWLCalCoeff_User().\nError String: Empty EEPROM.")
            self.write_coefficient_tree.delete(*self.write_coefficient_tree.get_children())
            return

        self.write_calpoint_tree.delete(*self.write_calpoint_tree.get_children())

        for i in range(sPointNum):
            self.write_calpoint_tree.insert("", "end", values=(str(dPixel[i]), str(dWavelength[i])))

    def combo_cell_wl_table(self,event=None):
        if self.combobox.current() == 0:
            dCoef = (c_double * 10)(*([0.0] * 10))
            order = 0

            # Execute the DLL function NReadWLCalCoeff_User
            sRtn = self.m_KSPLIB_NSeries.NReadWLCalCoeff_User(dCoef,order, self.sSelChannel)
            if sRtn == -113:
                error_message = f"Error Function: NReadWLCalCoeff_User().\nError String: {self.m_KSPLIB_NSeries.NGetErrorString(sRtn)}"
                messagebox.showerror("ERROR", error_message)
                return
            for i in range(self.parent.deviceInfo[self.selDev].iRealPixel):
                # Execute the DLL function NPolyCalc
                self.m_KSPLIB_NSeries.NPolyCalc(dCoef,order,i+1,self.dGetWLTable[i])

        else:
            dWavelength = (c_double * 30)(*([0.0] * 30))
            dPixel = (c_double * 30)(*([0.0] * 30))
            sPointNum = 0
            dTempCoef = (c_double * 30)(*([0.0] * 30))

            # Execute the DLL function NReadWLCalPoint_User
            sRtn = self.m_KSPLIB_NSeries.NReadWLCalPoint_User(dPixel,dWavelength,sPointNum,self.sSelChannel)
            if sRtn == -113:
                error_message = f"Error Function: NReadWLCalPoint_User().\nError String: {self.m_KSPLIB_NSeries.NGetErrorString(sRtn)}"
                messagebox.showerror("ERROR", error_message)
                return
            if sPointNum == 2:
                order = 1
            elif sPointNum == 3:
                order = 2
            else:
                order = 3

            # Execute the DLL function NPolyFit
            self.m_KSPLIB_NSeries.NPolyFit(dPixel,dWavelength,sPointNum,dTempCoef,order)
            for i in range(self.parent.deviceInfo[self.selDev].iRealPixel):
                # Execute the DLL function NPolyCalc
                self.m_KSPLIB_NSeries.NPolyCalc(dTempCoef,order,i + 1, self.dGetWLTable[i])

        for item in self.wavelength_table_tree.get_children():
            self.wavelength_table_tree.delete(item)

        for i in range(self.parent.deviceInfo[self.selDev].iRealPixel):
            self.wavelength_table_tree.insert("","end",value=(i+1,self.dGetWLTable[i]))

    def export_wl_table(self):
        # Check if there is any data in the Treeview
        if not self.wavelength_table_tree.get_children():
            messagebox.showwarning("No Data", "No data available to export.")
            return

        # Open file save dialog
        file_path = filedialog.asksaveasfilename(
            defaultextension=".csv",
            filetypes=[("CSV files", "*.csv"), ("All files", "*.*")],
            initialfile="WLTable.csv",
            title="Please specify the location to save"
        )

        # If the user has selected a file path
        if file_path:
            # Save the data to a CSV file
            with open(file_path, mode='w', newline='') as file:
                writer = csv.writer(file)

                # Write file header
                writer.writerow(["[Wavelength Table]"])
                writer.writerow([])  # Empty line
                writer.writerow(["Pixel", "Wavelength (nm)"])

                # Write data from Treeview
                for item in self.wavelength_table_tree.get_children():
                    row_data = self.wavelength_table_tree.item(item, "values")
                    writer.writerow(row_data)

            messagebox.showinfo("Save Successful", f"Data successfully saved to {file_path}")


    # Event ============================================================================================
    def start_edit(self, treeview, entry, event):
        # Get the selected item and column
        region = treeview.identify("region", event.x, event.y)
        if region != "cell":
            return
        self.row_id = treeview.identify_row(event.y)
        self.column = treeview.identify_column(event.x)

        # Get the bounding box (x, y position, width, and height) of the selected cell
        bbox = treeview.bbox(self.row_id, self.column)
        if bbox:
            x, y, width, height = bbox
            # Position the Entry widget exactly at the selected cell's location
            entry.place(x=x, y=y, width=width, height=height)

        # Get the current cell value and insert it into the Entry widget
        column_index = int(self.column[1]) - 1
        cell_value = treeview.item(self.row_id, "values")[column_index]
        entry.delete(0, tk.END)
        entry.insert(0, cell_value)
        entry.focus()

    def save_edit(self, treeview, entry, event):
        # Reflect the edited value in the Treeview
        new_value = entry.get()
        current_values = list(treeview.item(self.row_id, "values"))
        column_index = int(self.column[1]) - 1
        current_values[column_index] = new_value
        treeview.item(self.row_id, values=current_values)

        # If it's the last column, add a new empty row
        if column_index == len(treeview["columns"]) - 1:
            treeview.insert("", "end", values=tuple([""] * len(treeview["columns"])))

        # Hide the Entry widget after editing
        entry.place_forget()





class BurstMode(tk.Toplevel):
    def __init__(self,root,parent):
        if isinstance(parent, tk.Tk):
            super().__init__(root)
        else:
            raise ValueError("Parent must be an instance of tk.Tk")
        self.parent = parent
        self.selDev = self.parent.selDev
        self.sSelChannel = self.parent.deviceInfo[self.selDev].sChannel
        self.bSyncReadStop = True
        self.m_KSPLIB_NSeries = self.parent.m_KSPLIB_NSeries

        #=============== UI ========================

        self.title("Measurement Application")
        self.geometry("850x420")  # Adjust window size as needed

        # Measure Start Section
        measure_frame = tk.LabelFrame(self, text="Measure Start", padx=5, pady=5)
        measure_frame.place(x=5, y=10, width=180, height=120)

        tk.Label(measure_frame, text="Data Count:").place(x=5, y=5)
        self.data_count = tk.IntVar(value=10)
        self.data_count_spin = tk.Spinbox(measure_frame, from_=1, to=100, width=5,textvariable=self.data_count)
        self.data_count_spin.place(x=80, y=5)

        self.sync_trigger_mode = tk.BooleanVar()
        self.sync_trigger = tk.Checkbutton(measure_frame, text="Synchronous Trigger",variable = self.sync_trigger_mode,state="disabled")
        self.sync_trigger.place(x=5, y=30)

        self.start_button = tk.Button(measure_frame, text="Start",width = 5, height = 1, command= self.measure_Start)
        self.start_button.place(x=5, y=60)

        self.stop_button = tk.Button(measure_frame, text="Stop", width = 5, height = 1, command = self.stop_btn)
        self.stop_button.place(x=60, y=60)

        # Get Data From Memory Section
        memory_frame = tk.LabelFrame(self, text="Get Data From Memory", padx=5, pady=5)
        memory_frame.place(x=10, y=140, width=180, height=80)

        self.get_data_single = tk.Button(memory_frame, text="Get Data\n(Single)", width = 7, height = 2,command= self.btn_get_single_data_from_mem)
        self.get_data_single.place(x=5, y=5)

        self.get_data_all = tk.Button(memory_frame, text="Get Data\n(All)", width = 7, height= 2,command = self.btn_get_data_from_mem)
        self.get_data_all.place(x=75, y=5)

        # Save File Section
        save_frame = tk.LabelFrame(self, text="Save File", padx=5, pady=5)
        save_frame.place(x=10, y=230, width=180, height=60)

        self.export_csv_button = tk.Button(save_frame, text="Export Data CSV File", command= self.btn_export_click)
        self.export_csv_button.place(x=5, y=5)

        # Operation Status
        tk.Label(self, text="#Operation Status#").place(x=10, y=300)
        self.status_textbox = tk.Text(self, height=3, width=20)
        self.status_textbox.place(x=10, y=320, width=180, height=80)

        # Graph Area
        graph_frame = tk.Frame(self, bg="white")
        graph_frame.place(x=200, y=10, width=600, height=370)
        
        fig, self.ax = plt.subplots(figsize=(6, 3.7))

        canvas = FigureCanvasTkAgg(fig, master=graph_frame)
        canvas.draw()
        canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)


        # Bottom Scroll Bar
        self.scrollbar = tk.Scale(self, from_=1, to=1000, orient="horizontal", command = self.slide_event)
        self.scrollbar.place(x=200, y=375, width=600)
        if self.parent.deviceInfo[self.selDev].strModel.decode() in ["SM303NP", "SM303N"]:
            self.sync_trigger.config(state="normal")

    def measure_Start(self):
        if not self.bSyncReadStop:
            self.bSyncReadStop = True
            self.start_button.config(text = "Start")
            sRtn = self.m_KSPLIB_NSeries.NStopSyncTrgData(self.sSelChannel);
            return
        data_count_value = self.data_count.get()
        
        if data_count_value > 0:
            if self.sync_trigger_mode.get():
                if self.parent.deviceInfo[self.selDev].sTrgMode != 5:
                    sRtn = self.m_KSPLIB_NSeries.NSetTrgMode(5, self.sSelChannel)
                    
                self.parent.deviceInfo[self.selDev].sTrgMode = 5
                sRtn = self.m_KSPLIB_NSeries.NStartSyncTrgData(data_count_value, self.sSelChannel)
            else:
                sRtn = self.m_KSPLIB_NSeries.NStartBurstData(data_count_value, self.sSelChannel)

            if sRtn < 0:
                error_function = "NStartSyncTrgData" if self.sync_trigger_mode.get() else "NStartBurstData"

                error_message = f"Error Function : {error_function}.\nError String : {self.m_KSPLIB_NSeries.NGetErrorString(sRtn)}"
                messagebox.showerror("ERROR", error_message)
                return

            self.iReadCnt = data_count_value
            self.iLoadCnt = 0

            # Initialize data list
            self.m_lpDataList = [(c_int * self.parent.deviceInfo[self.selDev].iTotPixel)() for _ in range(self.iReadCnt)]

            self.scrollbar.config(from_=1, to=self.iReadCnt)

            if self.sync_trigger_mode.get():
                self.status_textbox.insert("end", f"\nSynchronous Trigger measurement start.\nRead Count : {self.iReadCnt}")
            else:
                self.status_textbox.insert("end", f"\nBurst Mode measurement start.\nRead Count : {self.iReadCnt}")
        elif data_count_value == 0:
            if self.sync_trigger_mode.get():
                if self.parent.deviceInfo[self.selDev].sTrgMode != 5:
                    sRtn = self.m_KSPLIB_NSeries.NSetTrgMode(5, self.sSelChannel)
                    self.parent.deviceInfo[self.selDev].sTrgMode = 5

                self.start_button.config(text="Stop")
                self.bSyncReadStop = False

                # Start continuous read thread
                self.SyncroContinousReadThrd = Thread(target=self.SynchroContinousReadFunc)
                self.SyncroContinousReadThrd.start()
            else:
                self.status_textbox.insert("end", "\nFor Burst Mode, Data Count must be greater than 0.")

    def SynchroContinousReadFunc(self):
        # Initialize the data array with the total number of pixels
        iDataArray = (c_int * self.parent.deviceInfo[self.selDev].iTotPixel)()

        while not self.bSyncReadStop:
            # Read data from the device
            sRtn = self.m_KSPLIB_NSeries.NReadDataEx(iDataArray, self.sSelChannel)
            if sRtn < 0:
                # Display an error message if the function fails
                error_message = f"Error Function: NReadDataEx().\nError String: {self.m_KSPLIB_NSeries.NGetErrorString(sRtn)}"
                messagebox.showerror("ERROR", error_message)
                break

            # Use tkinter's `after` method to safely update the UI
            self.root.after(0, self.update_graph_and_status, iDataArray)

    def update_graph_and_status(self, iDataArray):
        # Clear the current graph
        self.ax.clear()  # `self.s1` should be a `matplotlib` graph object

        # Get relevant data for graph plotting
        real_pixel = self.parent.deviceInfo[self.parent.selDev].iRealPixel
        wl_table = self.parent.deviceInfo[self.parent.selDev].dWLTable
        dummy_pixel = self.parent.deviceInfo[self.parent.selDev].iDummyPixel

        # Wavelengths and intensities for the current index
        wavelengths = wl_table[:real_pixel]
        intensities = [iDataArray[i + dummy_pixel] for i in range(real_pixel)]

        # Redraw the graph
        self.ax.figure.canvas.draw()
    
        # Update status text with additional information
        exposure_delay = iDataArray[1]
        trigger_interval = iDataArray[2]
        status_message = (
            "Successful data loaded from memory.\n"
            f"Exposure Delay: {exposure_delay} us\n"
            f"Trigger Interval: {trigger_interval} us"
        )
        # Update the status textbox
        self.status_textbox.delete('1.0', 'end')
        self.status_textbox.insert('end', status_message)

    def btn_get_single_data_from_mem(self):
        # Check if all data has been loaded
        if self.iReadCnt == self.iLoadCnt:
            messagebox.showerror("ERROR", "Error! Empty memory.\nFailed to load data from memory.")
            return

        # Get a single burst of data from memory
        sRtn = self.parent.m_KSPLIB_NSeries.NGetBurstSingleData(self.m_lpDataList[self.iLoadCnt], self.sSelChannel)
    
        if sRtn < 0:
            if sRtn == self.parent.KSPLIB_NSeries.emErrorCode.SP_ERROR_MEMORY_EMPTY:
                # Handle memory empty error
                messagebox.showerror("ERROR", f"Error! Empty memory.\nFailed to load data from memory.\nError String: {self.parent.m_KSPLIB_NSeries.NGetErrorString(sRtn)}")
            else:
                # Handle function call error
                messagebox.showerror("ERROR", f"Error Function: NGetBurstSingleData().\nError String: {self.parent.m_KSPLIB_NSeries.NGetErrorString(sRtn)}")
            return

        # Increment the load count and plot the chart
        self.iLoadCnt += 1
        self.plot_chart(self.iLoadCnt - 1)
    
        # Update the status message with information about the loaded data
        status_message = (f"Successful data loaded from memory.\nLoad Count: {self.iLoadCnt}/{self.iReadCnt}"
                                     f"\nCount: {self.m_lpDataList[self.iLoadCnt - 1][0]}"
                                     f"\nExposure Delay: {self.m_lpDataList[self.iLoadCnt - 1][1]} us"
                                     f"\nTrigger Interval: {self.m_lpDataList[self.iLoadCnt - 1][2]} us")
        self.status_textbox.delete('1.0', 'end')
        self.status_textbox.insert('end', status_message)

        # If all data is loaded, stop the sync trigger
        if self.iReadCnt == self.iLoadCnt:
            sRtn = self.parent.m_KSPLIB_NSeries.NStopSyncTrgData(self.sSelChannel)

    def btn_get_data_from_mem(self):
        data_count = int(self.data_count.get())
        lTempData = [(c_int * self.parent.deviceInfo[self.selDev].iTotPixel)() for _ in range(data_count)]  # Initialize a list of lists
        size = self.parent.deviceInfo[self.selDev].iTotPixel
        # Start the timer
        swReadData = time.perf_counter()  # Equivalent to Stopwatch

        # Call the function to get burst data
        sRtn = self.parent.m_KSPLIB_NSeries.NGetBurstData(data_count, lTempData, self.sSelChannel,size)

        # Stop the timer
        elapsed_time = time.perf_counter() - swReadData

        # Handle error conditions
        if sRtn < 0:
            if sRtn == -123:
                messagebox.showerror("ERROR", f"Error! Empty memory.\nFailed to load data from memory.\nError String: {self.parent.m_KSPLIB_NSeries.NGetErrorString(sRtn)}")
            else:
                messagebox.showerror("ERROR", f"Error Function: NGetBurstData().\nError String: {self.parent.m_KSPLIB_NSeries.NGetErrorString(sRtn)}")
            return

        # Copy the data to m_lpDataList
        for i in range(sRtn):
            self.m_lpDataList[self.iLoadCnt + i] = lTempData[i]

        self.iLoadCnt += sRtn

        # Plot the last loaded data if any data was returned
        if sRtn != 0:
            self.plot_chart(self.iLoadCnt - 1)

        # Update status information
        self.status_textbox.delete('1.0', 'end')
        self.status_textbox.insert('end', f"Successful data loaded from memory.\nLoad Count: {self.iLoadCnt}/{self.iReadCnt}")

        # If all data has been loaded, stop the sync trigger
        if self.iReadCnt == self.iLoadCnt:
            sRtn = self.parent.m_KSPLIB_NSeries.NStopSyncTrgData(self.sSelChannel)

    def slide_event(self,value):
        self.plot_chart(int(value)-1)
        status_info = (f"Measure Data Info : {self.iLoadCnt}/{self.iReadCnt}\n"
                           f"Count : {self.m_lpDataList[int(value) - 1][0]}\n"
                           f"Exposure Delay : {self.m_lpDataList[int(value) - 1][1]} us\n"
                           f"Trigger Interval : {self.m_lpDataList[int(value) - 1][2]} us")
        self.status_textbox.delete(1.0, tk.END)
        self.status_textbox.insert(tk.END, status_info)


    def plot_chart(self, iIndex):
        # Clear the existing graph
        self.ax.clear()

        # Get the necessary data for the graph
        real_pixel = self.parent.deviceInfo[self.parent.selDev].iRealPixel
        wl_table = self.parent.deviceInfo[self.parent.selDev].dWLTable
        dummy_pixel = self.parent.deviceInfo[self.parent.selDev].iDummyPixel

        # Wavelengths and intensities for the current index
        wavelengths = wl_table[:real_pixel]
        intensities = [self.m_lpDataList[iIndex][i + dummy_pixel] for i in range(real_pixel)]

        # Plot the graph with solid lines (blue color)
        self.ax.plot(wavelengths, intensities, 'b-')  # 'b-' indicates a blue solid line

        # Redraw the graph with updated data
        self.ax.figure.canvas.draw()

        # Update the data index in the scrollbar
        self.scrollbar.set(iIndex + 1)

    def btn_export_click(self):
        # Check if there's no data in memory
        if self.iReadCnt == 0 or self.iLoadCnt == 0:
            messagebox.showerror("ERROR", "Memory is empty because there is no measured data.")
            return

        # Open a file save dialog
        file_path = filedialog.asksaveasfilename(
            defaultextension=".csv",
            filetypes=[("CSV File", "*.csv")],
            initialfile="SynchronousTriggerDataFile.csv",
            title="Select Save File Path"
        )

        # If a file path was selected by the user
        if file_path:
            try:
                with open(file_path, mode='w', newline='') as swFile:
                    writer = csv.writer(swFile)

                    # Write the header row (wavelengths)
                    wl_table = self.parent.deviceInfo[self.selDev].dWLTable
                    real_pixel = self.parent.deviceInfo[self.selDev].iRealPixel
                    header = [''] + [str(wl_table[i]) for i in range(real_pixel)]
                    writer.writerow(header)

                    # Write the data rows
                    for i in range(self.iLoadCnt):
                        row_data = [i + 1]  # Data row starts with the index (1-based)
                        for j in range(real_pixel):
                            row_data.append(str(self.m_lpDataList[i][j + self.parent.deviceInfo[self.selDev].iDummyPixel]))
                        writer.writerow(row_data)

                # Update status text
                self.status_textbox.delete('1.0', 'end')
                self.status_textbox.insert('end', "\r\nSuccessful export csv file.")

            except Exception as e:
                messagebox.showerror("ERROR", f"Error occurred while exporting CSV file.\nError: {str(e)}")

    def stop_btn(self):
        sRtn = self.m_KSPLIB_NSeries.NStopSyncTrgData(self.sSelChannel)
        if sRtn < 0:
            messagebox.showerror("ERROR", f"Error Function : NStopSyncTrgData().\nError String : {self.parent.m_KSPLIB_NSeries.NGetErrorString(sRtn)}")



if __name__ == "__main__":
    root = tk.Tk()
    app = SDK_Net(root)
    root.mainloop()