- Plotting added - Statistic calculation added - Save measurement data as XLSX feature added - Test controller functionality extracted from the GUI class
379 lines
14 KiB
Python
379 lines
14 KiB
Python
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)
|
|
|