from collections.abc import Callable import time import tkinter as tk from tkinter import ttk from typing import Callable, Literal from matplotlib.axes import Axes from matplotlib.figure import Figure import matplotlib.pyplot as plt import numpy from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.ticker import MaxNLocator import tkinter.filedialog import Common import gui.GuiCommon as gcm import xlsxwriter # initialize plot style #plt.rcParams["font.family"] = "" plt.rcParams["font.size"] = 8 plt.rcParams["lines.antialiased"] = True plt.rcParams["lines.linewidth"] = 0.75 plt.rcParams["grid.color"] = "#262626" plt.rcParams["grid.alpha"] = 0.15 plt.rcParams["grid.linewidth"] = 0.5 plt.rcParams["legend.loc"] = "upper right" class ResultsInfoPanel(ttk.LabelFrame): def __init_style(self) -> None: style = ttk.Style() style.configure("MeasInfo.TLabel", font=gcm.TERMINAL_FONT) style.configure("MeasTitle.TLabel", font=("Ubuntu", 10, "bold")) style.configure("MeasStatPENDING.TFrame", background="Gray") style.configure("MeasStatIN-PROGRESS.TFrame", background="Gold") style.configure("MeasStatFAILED.TFrame", background="Salmon") style.configure("MeasStatERRORED.TFrame", background="firebrick4") style.configure("MeasStatPASSED.TFrame", background="LawnGreen") style.configure("MeasStatSKIPPED.TFrame", background="Gray") style.configure("MeasStatPENDING.TLabel", background="Gray") style.configure("MeasStatIN-PROGRESS.TLabel", background="Gold") style.configure("MeasStatFAILED.TLabel", background="Salmon") style.configure("MeasStatERRORED.TLabel", background="firebrick4") style.configure("MeasStatPASSED.TLabel", background="LawnGreen") style.configure("MeasStatSKIPPED.TLabel", background="Gray") def __init_layout(self) -> None: self.columnconfigure([0, 1], weight=1) def __init_widgets(self) -> None: # Start time start_label = ttk.Label(self, text="Start:") start_label.grid(row=0, column=0) # End time end_label = ttk.Label(self, text="End:") end_label.grid(row=1, column=0) # Duration duration_label = ttk.Label(self, text="Duration:") duration_label.grid(row=2, column=0) # separator separator1 = ttk.Separator(self) separator1.grid(row=3, column=0, columnspan=2, sticky=tk.EW) # Cycles label cycles_label = ttk.Label(self, text="Cycles:") cycles_label.grid(row=4, column=0) # Mean label mean_label = ttk.Label(self, text="Mean:") mean_label.grid(row=5, column=0) # Variance label variance_label = ttk.Label(self, text="Variance:") variance_label.grid(row=6, column=0) # Global maximum label gmax_label = ttk.Label(self, text="Max. [global]:") gmax_label.grid(row=7, column=0) # Global minimum label gmin_label = ttk.Label(self, text="Min. [global]:") gmin_label.grid(row=8, column=0) # Local maximum label lmax_label = ttk.Label(self, text="Max. [local]:") lmax_label.grid(row=9, column=0) # Local minimum label lmin_label = ttk.Label(self, text="Min. [local]:") lmin_label.grid(row=10, column=0) # separator separator2 = ttk.Separator(self) separator2.grid(row=11, column=0, columnspan=2, sticky=tk.EW) # ----------------- # Start time display self.__start_display = ttk.Label(self, text="-") self.__start_display.grid(row=0, column=1) # End time display self.__end_display = ttk.Label(self, text="-") self.__end_display.grid(row=1, column=1) # Duration display self.__duration_display = ttk.Label(self, text="-") self.__duration_display.grid(row=2, column=1) # Cycles display self.__cycles_display = ttk.Label(self, text="-") self.__cycles_display.grid(row=4, column=1) # Mean display self.__mean_display = ttk.Label(self, text="-") self.__mean_display.grid(row=5, column=1) # Variance display self.__variance_display = ttk.Label(self, text="-") self.__variance_display.grid(row=6, column=1) # Global maximum display self.__gmax_display = ttk.Label(self, text="-") self.__gmax_display.grid(row=7, column=1) # Global minimum display self.__gmin_display = ttk.Label(self, text="-") self.__gmin_display.grid(row=8, column=1) # Local maximum display self.__lmax_display = ttk.Label(self, text="-") self.__lmax_display.grid(row=9, column=1) # Local minimum display self.__lmin_display = ttk.Label(self, text="-") self.__lmin_display.grid(row=10, column=1) # ---------------- # apply styling for widget in self.winfo_children(): match widget: case ttk.Label(): widget.configure(style="MeasInfo.TLabel") widget.grid(sticky=tk.E) # ----------------- self.__status_frame = ttk.Frame(self) self.__status_frame.grid(row=12, column=0, columnspan=2, pady=8, sticky=tk.EW) self.__status_display = ttk.Label(self.__status_frame, text="-", font=("Ubuntu", 12, "bold")) self.__status_display.pack() # ------------------ self.__save_data_btn = ttk.Button(self, command=self.save_results, text="Save", state="disabled") self.__save_data_btn.grid(row=13, column=0, columnspan=2, pady=16, sticky=tk.EW + tk.S) def refresh(self) -> None: result = self.__test.get("result", "pending").upper() match result: case "IN-PROGRESS" | "PASSED" | "FAILED": cycles = self.__test["data"]["cycles"] self.__cycles_display["text"] = "{:d}".format(self.__test["data"]["cycles"]) if cycles > 0: self.__gmax_display["text"] = "{:d} ns".format(self.__test["data"]["max"]["global"]) self.__gmin_display["text"] = "{:d} ns".format(self.__test["data"]["min"]["global"]) if cycles > self.__test["data"]["skip"]: self.__variance_display["text"] = "{:.0f} ns^2".format(self.__test["data"]["var"][-1]) self.__mean_display["text"] = "{:.0f} ns".format(self.__test["data"]["mean"][-1]) self.__lmax_display["text"] = "{:d} ns".format(self.__test["data"]["max"]["local"]) self.__lmin_display["text"] = "{:d} ns".format(self.__test["data"]["min"]["local"]) match result: case "PENDING": pass case "IN-PROGRESS": self.__start_display["text"] = Common.time_to_str(self.__test.get("start", "0")) case "PASSED" | "FAILED": self.__start_display["text"] = Common.time_to_str(self.__test.get("start", "0")) self.__end_display["text"] = Common.time_to_str(self.__test.get("end", "0")) self.__duration_display["text"] = time.strftime("%H:%M:%S", time.gmtime(self.__test["duration"])) self.__status_display["text"] = result self.__status_display.configure(style="MeasStat{:s}.TLabel".format(result)) self.__status_frame.configure(style="MeasStat{:s}.TFrame".format(result)) def finalize(self) -> None: self.__save_data_btn.configure(state="normal") def __init__(self, master: tk.Widget, test: dict) -> None: super().__init__(master) self.__test = test self.__init_style() self.__init_layout() self.__init_widgets() self.configure(labelwidget=ttk.Label(self, text="Measurement info", style="MeasTitle.TLabel"), width=240) self.refresh() def save_results(self) -> None: # pick a place to save to fn = tkinter.filedialog.asksaveasfilename( title="Save measurement data", filetypes=[("XLSX", "*.xlsx")] ) if fn == (): return if not fn.endswith(".xlsx"): fn += ".xlsx" # -------------- # create the workbook workbook = xlsxwriter.Workbook(fn) # save the summary summary_sheet = workbook.add_worksheet("summary") summary = [ [ "Start:", Common.time_to_str(self.__test["start"])], [ "End:", Common.time_to_str(self.__test["end"]) ], [ "Duration:", time.strftime("%H:%M:%S", time.gmtime(self.__test["duration"])) ], [ "Cycles:", self.__test["data"]["cycles"] ], [ "Mean [ns]:", self.__test["data"]["mean"][-1] ], [ "Variance [ns^2]:", self.__test["data"]["var"][-1] ], [ "Max. (global) [ns]:", self.__test["data"]["max"]["global"] ], [ "Min. (global) [ns]:", self.__test["data"]["min"]["global"] ], [ "Max. (local) [ns]:", self.__test["data"]["max"]["local"] ], [ "Min. (local) [ns]:", self.__test["data"]["min"]["local"] ] ] rowi = 0 for summary_line in summary: summary_sheet.write_row(rowi, 0, summary_line) rowi += 1 # create the raw dump sheet dump_sheet = workbook.add_worksheet("raw_data") # write the header if self.__test["data"]["rawtype"] == "flexPTP": header = [ "T1", "T4", "Dt", "corr_ppb", "mpd_ns", "sync_period_ns" ] else: header = [ "Dt", "mpd_ns" ] dump_sheet.write_row(0, 0, header) # save actual sync cycle data rowi = 1 for record in self.__test["data"]["raw"]: dump_sheet.write_row(rowi, 0, record) rowi += 1 workbook.close() class PlotPanel(ttk.Frame): def __init_widgets(self, figsize: tuple) -> None: self.__fig, self.__ax = plt.subplots(figsize=figsize) self.__full_plot, = self.__ax.plot([], []) self.__ax.set_ylabel("Time error [ns]") self.__ax.set_xlabel("Cycles") self.__ax.grid(True) self.__ax.xaxis.set_major_locator(MaxNLocator(integer=True)) self.__ax.yaxis.set_major_locator(MaxNLocator(integer=True)) self.__full_plot_canvas = FigureCanvasTkAgg(self.__fig, master=self) # A tk.DrawingArea. style = ttk.Style() bg = style.lookup("TFrame", "background") self.__full_plot_canvas.get_tk_widget().config(bg=bg) self.__full_plot_canvas.get_tk_widget().pack(expand=True, fill="both") self.__fig.tight_layout() self.__fig.patch.set_alpha(0) def plot(self, start: int, stop: int, data: list[int]) -> None: self.__full_plot.set_xdata(range(start, stop)) self.__full_plot.set_ydata(data[start:stop]) self.__ax.relim() self.__ax.autoscale_view() self.draw() def draw(self) -> None: self.__full_plot_canvas.draw() self.__full_plot_canvas.flush_events() def get_ax(self) -> Axes: return self.__ax def get_fig(self) -> Figure: return self.__fig def __init__(self, master: tk.Widget, figsize: tuple = (8, 2)) -> None: super().__init__(master) self.__init_widgets(figsize) class ResultsEntry(ttk.LabelFrame): MIN_MAINPLOT_XRANGE = 100 MIN_LOOKIN_WINDOW_LENGTH = 5 def __init_layout(self) -> None: self.columnconfigure(0, weight=6) self.columnconfigure([1, 2], weight=2) def __init_widgets(self) -> None: # full measurement figure self.__main_figure = PlotPanel(self, figsize=(8, 3)) self.__main_figure.get_ax().set_xlim(0, self.MIN_MAINPLOT_XRANGE) self.__main_figure.grid(row=0, column=0, sticky=tk.NSEW) # look-in window self.__lookin_figure = PlotPanel(self, figsize=(2, 3)) self.__lookin_figure.get_ax().set_xlim(0, self.__lookin_window_size - 1) self.__lookin_figure.get_ax().set_ylabel("") self.__lookin_figure.get_ax().yaxis.tick_right() self.__lookin_figure.get_fig().subplots_adjust(left=0.05, right=0.78) self.__lookin_figure.grid(row=0, column=1, sticky=tk.NSEW) # info panel self.__info_panel = ResultsInfoPanel(self, self.__test) self.__info_panel.grid(row=0, column=2, sticky=tk.NSEW, ipadx=4, padx=4, pady=4) self.__info_panel.grid_propagate(False) def refresh(self) -> None: self.__info_panel.refresh() data = self.__test["data"] n = data["cycles"] self.__main_figure.get_ax().set_xlim(0, numpy.max([self.MIN_MAINPLOT_XRANGE, n, self.__params["sync_min_cycles"]])) self.__main_figure.plot(0, n, data["dt"]) if n > self.__lookin_window_size: n0 = n - self.__lookin_window_size self.__lookin_figure.get_ax().set_xlim(n0, n - 1) self.__lookin_figure.plot(n0, n, data["dt"]) def finalize(self) -> None: self.__main_figure.get_ax().set_xlim(0, self.__test["data"]["cycles"] - 1) self.__main_figure.draw() self.__info_panel.finalize() def __init__(self, master: tk.Widget, test: dict, params: dict) -> None: super().__init__(master) self.configure(labelwidget=ttk.Label(self, text=test["name"], font=gcm.TITLE_FONT)) self.__test = test self.__params = params self.__lookin_window_size = numpy.max([self.__params["averaging_window_size"], self.MIN_LOOKIN_WINDOW_LENGTH]) self.__init_layout() self.__init_widgets() class ResultsTab(ttk.Frame): def __init__(self, master: tk.Misc | None = None) -> None: super().__init__(master)