import json import os.path from packaging import version # import struct import threading import time import dbus import dbus.service from dbus.mainloop.glib import DBusGMainLoop import DBus_Basic import ccs_function_lib as cfl from typing import NamedTuple import confignator import gi import sys import matplotlib matplotlib.use('Gtk3Cairo') from matplotlib.figure import Figure # from matplotlib.backends.backend_gtk3cairo import FigureCanvasGTK3Cairo as FigureCanvas from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas from matplotlib.backends.backend_gtk3 import NavigationToolbar2GTK3 as NavigationToolbar import numpy as np from database.tm_db import DbTelemetryPool, DbTelemetry, scoped_session_maker # from sqlalchemy.sql.expression import func # from sqlalchemy.orm import load_only import importlib MPL_VERSION = version.parse(matplotlib._get_version()) cfg = confignator.get_config(check_interpolation=False) project = 'packet_config_{}'.format(cfg.get('ccs-database', 'project')) packet_config = importlib.import_module(project) TM_HEADER_LEN, TC_HEADER_LEN, PEC_LEN = [packet_config.TM_HEADER_LEN, packet_config.TC_HEADER_LEN, packet_config.PEC_LEN] gi.require_version('Gtk', '3.0') gi.require_version('Notify', '0.7') from gi.repository import Gtk, Gdk, GdkPixbuf, GLib, Notify, Pango # NOQA # from event_storm_squasher import delayed ActivePoolInfo = NamedTuple( 'ActivePoolInfo', [ ('filename', str), ('modification_time', int), ('pool_name', str), ('live', bool)]) REFRESH_RATE = 1 class PlotViewer(Gtk.Window): def __init__(self, loaded_pool, refresh_rate=REFRESH_RATE, parameters=None, start_live=False, **kwargs): Gtk.Window.__init__(self) assert isinstance(loaded_pool, str) Notify.init('PlotViewer') self.set_default_size(900, 560) self.set_title('Parameter Viewer') self.parameter_limits = set() self.data_dict = {} self.data_dict_info = {} # row idx of last data point in data_dict self.max_datapoints = 0 self.data_min_idx = None self.data_max_idx = None self.pi1_lut = {} self._pkt_buffer = {} # local store for TM packets extracted from SQL DB, for speedup self.cfg = confignator.get_config() # Set up the logger self.logger = cfl.start_logging('ParameterPlotter') self.refresh_rate = refresh_rate if not self.cfg.has_section(cfl.CFG_SECT_PLOT_PARAMETERS): self.cfg.add_section(cfl.CFG_SECT_PLOT_PARAMETERS) self.user_parameters = self.cfg[cfl.CFG_SECT_PLOT_PARAMETERS] self.session_factory_idb = scoped_session_maker('idb') self.session_factory_storage = scoped_session_maker('storage') # load specified pool res = self.session_factory_storage.execute('SELECT * FROM tm_pool WHERE pool_name="{}"'.format(loaded_pool)) try: iid, filename, protocol, modtime = res.fetchall()[0] self.loaded_pool = ActivePoolInfo(filename, modtime, os.path.basename(filename), bool(not filename.count('/'))) except IndexError: self.loaded_pool = None self.logger.error('Could not load pool {}'.format(loaded_pool)) raise NameError('Pool {} not found'.format(loaded_pool)) box = Gtk.VBox() self.add(box) hbox = Gtk.HBox() self.user_tm_decoders = cfl.user_tm_decoders_func() self.canvas = self.create_canvas() toolbar = self.create_toolbar() # self.loaded_pool) param_view = self.create_param_view() box.pack_start(toolbar, 0, 0, 3) box.pack_start(hbox, 1, 1, 0) hbox.pack_start(self.canvas, 1, 1, 0) hbox.pack_start(param_view, 0, 0, 0) navbar = self._create_navbar() box.pack_start(navbar, 0, 0, 0) # selection = self.treeview.get_selection() self.liveplot = self.live_plot_switch.get_active() # self.connect('delete-event', self.write_cfg) self.connect('delete-event', self.live_plot_off) if parameters is None: parameters = {} self.plot_parameters = parameters if len(parameters) != 0: for hk in parameters: for par in parameters[hk]: self.plot_parameter(parameter=(hk, par)) self.live_plot_switch.set_active(start_live) self.show_all() def create_toolbar(self): toolbar = Gtk.HBox() self.pool_label = Gtk.Label(tooltip_text=self.loaded_pool.filename, ellipsize=Pango.EllipsizeMode.MIDDLE) self.pool_label.set_markup('<span foreground=\"#656565\">{}</span>'.format(self.loaded_pool.pool_name)) toolbar.pack_start(self.pool_label, 0, 0, 10) toolbar.pack_start(Gtk.Separator.new(Gtk.Orientation.VERTICAL), 0, 0, 0) self.filter_tl2 = Gtk.CheckButton(label='t<2', active=True) self.filter_tl2.set_tooltip_text("Plot datapoints with CUC time < 2") toolbar.pack_start(self.filter_tl2, 0, 0, 0) self.linlog = Gtk.CheckButton(label='logscale') self.linlog.set_tooltip_text('Toggle y-axis scale') self.linlog.connect("toggled", self.toggle_yscale) toolbar.pack_start(self.linlog, 0, 0, 0) self.scaley = Gtk.CheckButton(label='Fix Y axis', active=False) self.scaley.set_tooltip_text("If enabled, don't rescale Y axis when new parameter is plotted.") toolbar.pack_start(self.scaley, 0, 0, 0) self.show_legend = Gtk.CheckButton(label='Legend', active=True) self.show_legend.set_tooltip_text('Show/hide legend') self.show_legend.connect("toggled", self.toggle_legend) toolbar.pack_start(self.show_legend, 0, 0, 0) self.show_limits = Gtk.CheckButton(label='Limits', active=False) self.show_limits.set_tooltip_text('Show/hide parameter limits') self.show_limits.connect("toggled", self._toggle_limits) toolbar.pack_start(self.show_limits, 0, 0, 0) self.calibrate = Gtk.CheckButton(label='Cal', active=True) self.calibrate.set_tooltip_text('Plot engineering values, if available') # self.calibrate.connect("toggled", self._toggle_limits) toolbar.pack_start(self.calibrate, 0, 0, 0) # toolbar.pack_start(Gtk.Separator.new(Gtk.Orientation.VERTICAL), 0, 0, 5) # max_data_label = Gtk.Label(label='#') # max_data_label.set_tooltip_text('Plot at most ~NMAX data points (0 for unlimited), between MIN and MAX packet indices.') # self.max_data = Gtk.Entry() # self.max_data.set_width_chars(6) # self.max_data.set_alignment(1) # self.max_data.set_placeholder_text('NMAX') # self.max_data.set_input_purpose(Gtk.InputPurpose.DIGITS) # self.max_data.connect('activate', self._set_max_datapoints) # self.max_data.set_tooltip_text('At most ~NMAX data points plotted (0 for unlimited)') # toolbar.pack_start(max_data_label, 0, 0, 3) # toolbar.pack_start(self.max_data, 0, 0, 0) self.min_idx = Gtk.Entry() self.min_idx.set_width_chars(7) self.min_idx.set_alignment(1) self.min_idx.set_placeholder_text('MIN') self.min_idx.set_input_purpose(Gtk.InputPurpose.DIGITS) self.min_idx.set_tooltip_text('Get parameters starting from packet index') self.max_idx = Gtk.Entry() self.max_idx.set_width_chars(7) self.max_idx.set_alignment(1) self.max_idx.set_placeholder_text('MAX') self.max_idx.set_input_purpose(Gtk.InputPurpose.DIGITS) self.max_idx.set_tooltip_text('Get parameters up to packet index') toolbar.pack_start(self.min_idx, 0, 0, 0) toolbar.pack_start(self.max_idx, 0, 0, 0) self.live_plot_switch = Gtk.Switch() self.live_plot_switch.set_tooltip_text('Toggle real time parameter plotting') self.live_plot_switch.connect("state-set", self.on_switch_liveplot) live_plot_label = Gtk.Label(label='Live plot:') live_plot = Gtk.HBox() live_plot.pack_start(live_plot_label, 0, 0, 5) live_plot.pack_start(self.live_plot_switch, 0, 0, 0) univie_box = self.create_univie_box() toolbar.pack_end(univie_box, 0, 0, 0) toolbar.pack_end(live_plot, 0, 0, 0) return toolbar def create_canvas(self): fig = Figure() self.subplot = fig.add_subplot(111) self.subplot.grid() self.subplot.set_xlabel('CUC time [s]') self.subplot.callbacks.connect('xlim_changed', self._update_plot_xlimit_values) self.subplot.callbacks.connect('ylim_changed', self._update_plot_ylimit_values) canvas = FigureCanvas(fig) canvas.set_size_request(500, 500) return canvas def _create_navbar(self): # window argument to be removed if MPL_VERSION < version.parse('3.6.0'): navbar = NavigationToolbar(self.canvas, self) else: navbar = NavigationToolbar(self.canvas) limits = Gtk.HBox() self.xmin = Gtk.Entry() self.xmin.set_width_chars(9) self.xmin.connect('activate', self.set_plot_limits) xmin_label = Gtk.Label(label='xmin:') self.xmax = Gtk.Entry() self.xmax.set_width_chars(9) self.xmax.connect('activate', self.set_plot_limits) xmax_label = Gtk.Label(label='xmax:') self.ymin = Gtk.Entry() self.ymin.connect('activate', self.set_plot_limits) self.ymin.set_width_chars(9) ymin_label = Gtk.Label(label='ymin:') self.ymax = Gtk.Entry() self.ymax.set_width_chars(9) self.ymax.connect('activate', self.set_plot_limits) ymax_label = Gtk.Label(label='ymax:') [i.set_text('{:.1f}'.format(j)) for j, i in zip(self.subplot.get_xlim() + self.subplot.get_ylim(), (self.xmin, self.xmax, self.ymin, self.ymax))] limits.pack_start(xmin_label, 0, 0, 0) limits.pack_start(self.xmin, 0, 0, 2) limits.pack_start(xmax_label, 0, 0, 0) limits.pack_start(self.xmax, 0, 0, 2) limits.pack_start(ymin_label, 0, 0, 0) limits.pack_start(self.ymin, 0, 0, 2) limits.pack_start(ymax_label, 0, 0, 0) limits.pack_start(self.ymax, 0, 0, 2) limitbox = Gtk.ToolItem() limitbox.add(limits) navbar.insert(limitbox, 9) return navbar def create_param_view(self): self.treeview = Gtk.TreeView(model=self.create_parameter_model()) self.treeview.append_column(Gtk.TreeViewColumn("Parameters", Gtk.CellRendererText(), text=0)) sw = Gtk.ScrolledWindow() sw.set_size_request(270, -1) # workaround for allocation warning GTK bug # grid = Gtk.Grid() # grid.attach(self.treeview, 0, 0, 1, 1) # sw.add(grid) sw.add(self.treeview) bbox = Gtk.HBox(homogeneous=True) add_button = Gtk.Button(label='Add') add_button.connect('clicked', self.plot_parameter) clear_button = Gtk.Button(label='Clear') clear_button.connect('clicked', self.clear_parameter) self.plot_diff = Gtk.CheckButton(label='DIFF', active=False) self.plot_diff.set_tooltip_text('Plot difference between consecutive parameter values') bbox.pack_start(add_button, 1, 1, 0) bbox.pack_start(clear_button, 1, 1, 0) bbox.pack_start(self.plot_diff, 0, 0, 0) hbox = Gtk.HBox(homogeneous=True) data_button = Gtk.Button(label='View plot data') data_button.set_image(Gtk.Image.new_from_icon_name('gtk-justify-fill', Gtk.IconSize.BUTTON)) data_button.set_always_show_image(True) data_button.connect('clicked', self.show_plot_data) save_button = Gtk.Button(label='Save plot data') save_button.set_image(Gtk.Image.new_from_icon_name('gtk-save', Gtk.IconSize.BUTTON)) save_button.set_always_show_image(True) save_button.connect('clicked', self.save_plot_data) hbox.pack_start(data_button, 1, 1, 0) hbox.pack_start(save_button, 1, 1, 0) box = Gtk.HBox() add_userpar_butt = Gtk.Button(label='Add User Defined Parameter') add_userpar_butt.connect('clicked', self.add_user_parameter, self.treeview) edit_userpar_butt = Gtk.Button() edit_userpar_butt.set_image(Gtk.Image.new_from_icon_name('gtk-edit', Gtk.IconSize.BUTTON)) edit_userpar_butt.connect('clicked', self.edit_user_parameter, self.treeview) edit_userpar_butt.set_tooltip_text('Edit user defined parameter') rm_userpar_butt = Gtk.Button() rm_userpar_butt.set_image(Gtk.Image.new_from_icon_name('list-remove', Gtk.IconSize.BUTTON)) rm_userpar_butt.connect('clicked', self.remove_user_parameter, self.treeview) rm_userpar_butt.set_tooltip_text('Remove user defined parameter') box.pack_start(add_userpar_butt, 1, 1, 0) box.pack_start(edit_userpar_butt, 0, 0, 0) box.pack_start(rm_userpar_butt, 0, 0, 0) vbox = Gtk.VBox() vbox.pack_start(box, 0, 0, 0) vbox.pack_start(sw, 1, 1, 0) vbox.pack_start(bbox, 0, 0, 0) vbox.pack_start(hbox, 0, 0, 0) return vbox def create_parameter_model(self): parameter_model = Gtk.TreeStore(str) self.store = parameter_model dbcon = self.session_factory_idb dbres = dbcon.execute('SELECT pid_descr,pid_spid,pid_type from pid order by pid_type,pid_stype,pid_pi1_val') hks = dbres.fetchall() topleveliters = {} for hk in hks: if not hk[2] in topleveliters: serv = parameter_model.append(None, ['Service ' + str(hk[2])]) topleveliters[hk[2]] = serv it = parameter_model.append(topleveliters[hk[2]], [hk[0]]) dbres = dbcon.execute('SELECT pcf.pcf_descr from pcf left join plf on pcf.pcf_name=plf.plf_name left join pid on \ plf.plf_spid=pid.pid_spid where pid.pid_spid={} ORDER BY pcf.pcf_descr'.format(hk[1])) params = dbres.fetchall() for par in params: parameter_model.append(it, [par[0]]) dbcon.close() # add user defined PACKETS topit = parameter_model.append(None, ['UDEF']) for hk in self.user_tm_decoders: it = parameter_model.append(topit, ['UDEF|{}'.format(self.user_tm_decoders[hk][0])]) for par in self.user_tm_decoders[hk][1]: parameter_model.append(it, [par[1]]) # add user defined PARAMETERS self.useriter = parameter_model.append(None, ['User defined']) for userpar in self.cfg[cfl.CFG_SECT_PLOT_PARAMETERS]: parameter_model.append(self.useriter, [userpar]) return parameter_model def add_user_parameter(self, widget, treeview): parameter_model = treeview.get_model() param_values = cfl.add_user_parameter(parentwin=self) if param_values: label, apid, st, sst, sid, bytepos, fmt, offbi = param_values self.user_parameters[label] = json.dumps( {'APID': apid, 'ST': st, 'SST': sst, 'SID': sid, 'bytepos': bytepos, 'format': fmt, 'offbi': offbi}) parameter_model.append(self.useriter, [label]) def remove_user_parameter(self, widget, treeview): selection = treeview.get_selection() model, parpath = selection.get_selected_rows() # parameter_model = treeview.get_model() try: if model[parpath].parent is not None and model[parpath].parent[0] == 'User defined': # Check if selection is an object or the parent tab is selected parname = model[parpath][0] param_values = cfl.remove_user_parameter(parname) else: param_values = None except Exception as err: self.logger.warning(err) # param_values = cfl.remove_user_parameter(parentwin=self) return if param_values: parameter_model = self.treeview.get_model() self.user_parameters.pop(param_values) parameter_model.remove(self.useriter) self.useriter = self.store.append(None, ['User defined']) for userpar in self.cfg[cfl.CFG_SECT_PLOT_PARAMETERS]: parameter_model.append(self.useriter, [userpar]) def edit_user_parameter(self, widget, treeview): selection = treeview.get_selection() model, parpath = selection.get_selected_rows() try: if model[parpath].parent is not None and model[parpath].parent[0] == 'User defined': # Check if selection is an object or the parent tab is selected parname = model[parpath][0] param_values = cfl.edit_user_parameter(self, parname) if param_values: self.user_parameters.pop(parname) label, apid, st, sst, sid, bytepos, fmt, offbi = param_values self.user_parameters[label] = json.dumps( {'APID': apid, 'ST': st, 'SST': sst, 'SID': sid, 'bytepos': bytepos, 'format': fmt, 'offbi': offbi}) model[parpath][0] = label else: return # param_values = cfl.edit_user_parameter(self) # if param_values: # label, apid, st, sst, sid, bytepos, fmt, offbi = param_values # self.user_parameters[label] = json.dumps( # {'APID': apid, 'ST': st, 'SST': sst, 'SID': sid, 'bytepos': bytepos, 'format': fmt, 'offbi': offbi}) except Exception as err: self.logger.warning(err) return def create_univie_box(self): """ Creates the Univie Button which can be found in every application, Used to Start all parts of the CCS and manage communication :return: """ univie_box = Gtk.HBox() univie_button = Gtk.ToolButton() # button_run_nextline.set_icon_name("media-playback-start-symbolic") pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size( self.cfg.get('paths', 'ccs') + '/pixmap/Icon_Space_blau_en.png', 24, 24) icon = Gtk.Image.new_from_pixbuf(pixbuf) univie_button.set_icon_widget(icon) univie_button.set_tooltip_text('Applications and About') univie_button.connect("clicked", self.on_univie_button) univie_box.add(univie_button) # Popover creates the popup menu over the button and lets one use multiple buttons for the same one self.popover = Gtk.Popover() # Add the different Starting Options vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5, margin=4) for name in self.cfg['ccs-dbus_names']: # don't provide "start plotter" if name in ['plotter', 'monitor']: continue start_button = Gtk.Button.new_with_label("Start " + name.capitalize()) start_button.connect("clicked", cfl.on_open_univie_clicked) vbox.pack_start(start_button, False, True, 0) # Add the manage connections option conn_button = Gtk.Button.new_with_label('Communication') conn_button.connect("clicked", self.on_communication_dialog) vbox.pack_start(conn_button, False, True, 0) # Add the option to see the Credits about_button = Gtk.Button.new_with_label('About') about_button.connect("clicked", self._on_select_about_dialog) vbox.pack_start(about_button, False, True, 10) self.popover.add(vbox) self.popover.set_position(Gtk.PositionType.BOTTOM) self.popover.set_relative_to(univie_button) return univie_box def on_univie_button(self, action): """ Adds the Popover menu to the UNIVIE Button :param action: Simply the button :return: """ self.popover.show_all() self.popover.popup() def on_communication_dialog(self, button): cfl.change_communication_func(main_instance=self.main_instance, parentwin=self) def _on_select_about_dialog(self, action): cfl.about_dialog(self) return def get_active_pool_name(self): # return self.pool_selector.get_active_text() return self.loaded_pool.filename def sid_position_query(self, st, sst, apid, sid): if (st, sst, apid) in self.pi1_lut: sid_offset, sid_bitlen = self.pi1_lut[(st, sst, apid)] else: # dbcon = self.session_factory_idb # que = 'SELECT PIC_PI1_OFF, PIC_PI1_WID FROM pic WHERE PIC_TYPE ="{}" AND PIC_STYPE ="{}" AND PIC_APID ="{}"'.format(st, sst, apid) # dbres = dbcon.execute(que) # sid_offset, sid_bitlen = dbres.fetchall()[0] # dbcon.close() sidinfo = cfl.get_sid(st, sst, apid) if sidinfo: sid_offset, sid_bitlen = sidinfo self.pi1_lut[(st, sst, apid)] = (sid_offset, sid_bitlen) else: return if sid_offset == -1 or sid == 0: return # sid_search = b'' # i = 0 # while i < sid_offset: # i += 1 # sid_search += b'_' # # sid_search += struct.pack('>' + pi1_length_in_bits[sid_length], sid) # sid_search += b'%' return sid_offset, sid_bitlen // 8 def plot_parameter(self, widget=None, parameter=None): nocal = not self.calibrate.get_active() if parameter is not None: hk, parameter = parameter else: selection = self.treeview.get_selection() model, treepath = selection.get_selected() if treepath is None: return parameter = model[treepath][0] if model[treepath].parent is None: return hk = model[treepath].parent[0] rows = cfl.get_pool_rows(self.loaded_pool.filename) rows = self.set_plot_range(rows) dbcon = self.session_factory_idb if hk != 'User defined' and not hk.startswith('UDEF|'): que = 'SELECT pid_type,pid_stype,pid_pi1_val,pid_apid FROM pid LEFT JOIN plf ON pid.pid_spid=plf.plf_spid ' \ 'LEFT JOIN pcf ON plf.plf_name=pcf.pcf_name WHERE pcf.pcf_descr="{}" AND ' \ 'pid.pid_descr="{}"'.format(parameter, hk) dbres = dbcon.execute(que).fetchall() if not dbres: self.logger.error('{} is not a valid parameter.'.format(parameter)) return st, sst, sid, apid = dbres[0] elif hk.startswith('UDEF|'): label = hk.replace('UDEF|', '') tag = [k for k in self.user_tm_decoders if self.user_tm_decoders[k][0] == label][0] pktinfo = tag.split('-') st = int(pktinfo[0]) sst = int(pktinfo[1]) apid = int(pktinfo[2]) if pktinfo[2] != 'None' else None sid = int(pktinfo[3]) if pktinfo[3] != 'None' else None else: userpar = json.loads(self.cfg[cfl.CFG_SECT_PLOT_PARAMETERS][parameter]) st, sst, apid = userpar['ST'], userpar['SST'], userpar['APID'] if 'SID' in userpar and userpar['SID']: sid = userpar['SID'] else: sid = None if self.sid_position_query(st, sst, apid, sid) is None: if sid: self.logger.error('{}: SID not applicable.'.format(parameter)) return sid = None rows = cfl.filter_rows(rows, st=st, sst=sst, apid=apid, sid=sid) if not self.filter_tl2.get_active(): rows = cfl.filter_rows(rows, time_from=2.) # rows = rows.filter(func.left(DbTelemetry.timestamp, func.length(DbTelemetry.timestamp) - 1) > 2.) try: # TODO: speedup? if hk in self._pkt_buffer: bufidx, pkts = self._pkt_buffer[hk] rows = cfl.filter_rows(rows, idx_from=bufidx+1) if rows.first() is not None: bufidx = rows.order_by(DbTelemetry.idx.desc()).first().idx pkts += [row.raw for row in rows.yield_per(1000)] self._pkt_buffer[hk] = (bufidx, pkts) else: pkts = [row.raw for row in rows.yield_per(1000)] if len(pkts) > 0: bufidx = rows.order_by(DbTelemetry.idx.desc()).first().idx self._pkt_buffer[hk] = (bufidx, pkts) xy, (descr, unit) = cfl.get_param_values(tmlist=pkts, hk=hk, param=parameter, numerical=True, tmfilter=False, nocal=nocal) if len(xy) == 0: return except (ValueError, TypeError) as err: self.logger.debug(err) self.logger.error("Can't plot {}".format(parameter)) return # store packet info for update worker self.data_dict[hk + ':' + descr] = xy self.data_dict_info[hk + ':' + descr] = {} self.data_dict_info[hk + ':' + descr]['idx_last'] = bufidx # self.data_dict_info[hk + ':' + descr]['idx_last'] = rows.order_by(DbTelemetry.idx.desc()).first().idx self.data_dict_info[hk + ':' + descr]['st'] = st self.data_dict_info[hk + ':' + descr]['sst'] = sst self.data_dict_info[hk + ':' + descr]['apid'] = apid self.data_dict_info[hk + ':' + descr]['sid'] = sid # npoints = self.count_datapoints(self.subplot.get_xlim(), self.subplot.get_ylim()) # if npoints > self.max_datapoints > 0: # xy = xy.T[::npoints // self.max_datapoints + 1].T self.subplot.autoscale(enable=not self.scaley.get_active(), axis='y') try: if self.plot_diff.get_active(): x, y = xy x1 = x[1:] dy = np.diff(y) line = self.subplot.plot(x1, dy, marker='.', label=descr, gid=hk) else: line = self.subplot.plot(*xy, marker='.', label=descr, gid=hk) except TypeError: self.logger.error("Can't plot data of type {}".format(xy.dtype[1])) return self.reduce_datapoints(self.subplot.get_xlim(), self.subplot.get_ylim(), fulldata=False) # draw limits if available dbres = dbcon.execute('SELECT pcf.pcf_name, pcf.pcf_descr, pcf.pcf_categ, pcf.pcf_unit, ocf.ocf_nbool,\ ocp.ocp_lvalu, ocp.ocp_hvalu from pcf left join ocf on\ pcf.pcf_name=ocf.ocf_name left join ocp on ocf_name=ocp_name\ where pcf.pcf_descr="{}"'.format(parameter)) limits = dbres.fetchall() dbcon.close() try: nlims = limits[0][-3] if nlims is not None: if nlims == 1: param_id, plabel, fmt, unit, _, lolim, hilim = limits[0] hardlim = (float(lolim), float(hilim)) softlim = (None, None) else: param_id, plabel, fmt, unit = limits[0][:4] softlim, hardlim = [(float(x[-2]), float(x[-1])) for x in limits] show_limits = self.show_limits.get_active() if softlim != (None, None): for pos, y in zip(('lo', 'hi'), softlim): limitline = self.subplot.axhline(y, color=line[0].get_color(), alpha=0.5, ls=':', label='_lim_soft_{}_{}'.format(pos, parameter)) limitline.set_visible(show_limits) self.parameter_limits.add(limitline) for pos, y in zip(('lo', 'hi'), hardlim): limitline = self.subplot.axhline(y, color=line[0].get_color(), alpha=0.5, ls='--', label='_lim_hard_{}_{}'.format(pos, parameter)) limitline.set_visible(show_limits) self.parameter_limits.add(limitline) except IndexError: self.logger.info('Parameter {} does not have limits to plot'.format(parameter)) # self.subplot.fill_between([-1e9,1e9],[1,1],[2,2],facecolor='orange',alpha=0.5,hatch='/') # self.subplot.fill_between([-1e9,1e9],2,10,facecolor='red',alpha=0.5) self.subplot.legend(loc=2, framealpha=0.5) # bbox_to_anchor=(0.,1.02,1.,.102),mode="expand", borderaxespad=0) if self.subplot.get_legend() is not None: self.subplot.get_legend().set_visible(self.show_legend.get_active()) self.subplot.set_ylabel('[{}]'.format(unit)) self.canvas.draw() def set_plot_range(self, rows): try: new_min_idx = int(self.min_idx.get_text()) if new_min_idx != self.data_min_idx: self.data_min_idx = new_min_idx self._pkt_buffer = {} rows = rows.filter(DbTelemetry.idx >= self.data_min_idx) except (TypeError, ValueError): self.data_min_idx = None try: new_max_idx = int(self.max_idx.get_text()) if new_max_idx != self.data_max_idx: self.data_max_idx = new_max_idx self._pkt_buffer = {} rows = rows.filter(DbTelemetry.idx <= self.data_max_idx) except (TypeError, ValueError): self.data_max_idx = None # try: # self.max_datapoints = int(self.max_data.get_text()) # except (TypeError, ValueError): # self.max_datapoints = 0 return rows def _toggle_limits(self, widget=None): if widget.get_active(): for line in self.parameter_limits: line.set_visible(1) else: for line in self.parameter_limits: line.set_visible(0) self.canvas.draw() # def _set_max_datapoints(self, widget=None): # try: # n = int(widget.get_text()) # if n < 0: # widget.set_text('0') # n = 0 # except (TypeError, ValueError): # if widget.get_text() == '': # n = 0 # widget.set_text('0') # else: # widget.set_text('0') # return # self.max_datapoints = n def reduce_datapoints(self, xlim, ylim, fulldata=True): ax = self.canvas.figure.get_axes()[0] if self.max_datapoints > 0: n_datapoints = self.count_datapoints(xlim, ylim) if n_datapoints > self.max_datapoints: red_fac = n_datapoints // self.max_datapoints + 1 for line in ax.lines: if not line.get_label().startswith('_lim_'): x, y = self.data_dict[line.get_gid() + ':' + line.get_label()] if self.plot_diff.get_active(): x = x[1:] y = np.diff(y) line.set_xdata(x[::red_fac]) line.set_ydata(y[::red_fac]) elif fulldata: for line in ax.lines: if not line.get_label().startswith('_lim_'): x, y = self.data_dict[line.get_gid() + ':' + line.get_label()] if self.plot_diff.get_active(): x = x[1:] y = np.diff(y) line.set_xdata(x) line.set_ydata(y) def count_datapoints(self, xlim, ylim): try: n = sum([len(np.where((xlim[0] < x) & (x < xlim[1]) & (ylim[0] < y) & (y < ylim[1]))[0]) for x, y in self.data_dict.values()]) except ValueError: n = 0 # self.max_data.set_tooltip_text('{} datapoints'.format(n)) return n def clear_parameter(self, widget): self.data_dict.clear() self.data_dict_info.clear() self.parameter_limits.clear() self.subplot.clear() self.subplot.grid() self.subplot.set_xlabel('CUC Time [s]') self.subplot.callbacks.connect('xlim_changed', self._update_plot_xlimit_values) self.subplot.callbacks.connect('ylim_changed', self._update_plot_ylimit_values) self._update_plot_xlimit_values() self._update_plot_ylimit_values() self.canvas.draw() def update_plot_worker(self, plot=None, parameter=None): # pool_name = self.pool_box.get_active_text() rows = cfl.get_pool_rows(self.loaded_pool.filename) rows = self.set_plot_range(rows) # xmin, xmax = self.subplot.get_xlim() lines = self.subplot.lines nocal = not self.calibrate.get_active() for line in lines: parameter = line.get_label() if not parameter.startswith('_lim_'): hk = line.get_gid() xold, yold = self.data_dict[hk + ':' + parameter] # time_last = round(float(xold[-1]), 6) # np.float64 not properly understood in sql comparison below # new_rows = rows.filter(func.left(DbTelemetry.timestamp, func.length(DbTelemetry.timestamp) - 1) > time_last) pinfo = self.data_dict_info[hk + ':' + parameter] new_rows = cfl.filter_rows(rows, st=pinfo['st'], sst=pinfo['sst'], apid=pinfo['apid'], sid=pinfo['sid'], idx_from=pinfo['idx_last'] + 1) try: # xnew, ynew = cfl.get_param_values([row.raw for row in new_rows], hk, parameter, numerical=True)[0] xnew, ynew = cfl.get_param_values(tmlist=[row.raw for row in new_rows], hk=hk, param=parameter, numerical=True, tmfilter=False, nocal=nocal)[0] idx_new = new_rows.order_by(DbTelemetry.idx.desc()).first().idx except ValueError: continue xy = np.stack([np.append(xold, xnew), np.append(yold, ynew)], -1).T self.data_dict[hk + ':' + parameter] = xy self.data_dict_info[hk + ':' + parameter]['idx_last'] = idx_new self.reduce_datapoints(self.subplot.get_xlim(), self.subplot.get_ylim()) def set_view(): self.subplot.autoscale(enable=not self.scaley.get_active(), axis='y') self.subplot.relim() self.subplot.autoscale_view() self.canvas.draw() GLib.idle_add(set_view, priority=GLib.PRIORITY_HIGH) def set_plot_limits(self, widget): limitbox = widget.get_parent() limits = [x.get_text() for x in limitbox.get_children()[1::2]] xmin, xmax, ymin, ymax = map(float, limits) self.subplot.set_xlim(xmin, xmax) self.subplot.set_ylim(ymin, ymax) self.reduce_datapoints((xmin, xmax), (ymin, ymax)) self.canvas.draw() def _update_plot_xlimit_values(self, axes=None): if axes is None: axes = self.subplot xlim = axes.get_xlim() self.xmin.set_text(str(xlim[0])) self.xmax.set_text(str(xlim[1])) def _update_plot_ylimit_values(self, axes=None): if axes is None: axes = self.subplot ylim = axes.get_ylim() self.ymin.set_text(str(ylim[0])) self.ymax.set_text(str(ylim[1])) def toggle_yscale(self, button): active = button.get_active() if active: self.subplot.set_yscale('log') self.canvas.draw() else: self.subplot.set_yscale('linear') self.canvas.draw() def toggle_legend(self, button): active = button.get_active() if self.subplot.get_legend(): self.subplot.get_legend().set_visible(active) self.canvas.draw() def on_switch_liveplot(self, widget, onoff=None): self.liveplot = onoff if onoff: thread = threading.Thread(target=self.update_plot) thread.name = 'Plot-updater' thread.daemon = True thread.start() def update_plot(self): while self.liveplot: t1 = time.time() # GLib.idle_add(self.update_plot_worker, priority=GLib.PRIORITY_HIGH) self.update_plot_worker() dt = self.refresh_rate - (time.time() - t1) if dt > 0: time.sleep(dt) def set_refresh_rate(self, rate): self.refresh_rate = rate def save_plot_data(self, widget=None, data=None, filename=None): def save(fname): d = {} # for line in self.subplot.lines: # parameter = line.get_label() # if not parameter.startswith('_lim_'): # hk = line.get_gid() # xy = line.get_xydata() # try: # d[hk][parameter] = xy # except KeyError: # d.setdefault(hk, {parameter: xy}) for parameter in self.data_dict: hk, param = parameter.split(':') try: d[hk][param] = self.data_dict[parameter].T except KeyError: d.setdefault(hk, {param: self.data_dict[parameter].T}) hkblocks = [] for n in d: params = list(d[n].keys()) head = '# {}\n# CUC_Time\t'.format(n) + '\t'.join(params) + '\n' datablock = '\n'.join( ['{:.6F}\t'.format( d[n][params[0]][i, 0]) + '\t'.join(['{:.12G}'.format(d[n][param][i, 1]) for param in params]) for i in range(len(d[n][params[0]][:, 1]))]) hkblocks.append(head + datablock) with open(fname, 'w') as fdesc: fdesc.write('# Source: {}\n'.format(self.loaded_pool.pool_name) + '\n\n'.join(hkblocks)) if filename: save(filename) return else: dialog = Gtk.FileChooserDialog(title="Save data as", parent=self, action=Gtk.FileChooserAction.SAVE) dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK) dialog.set_transient_for(self) response = dialog.run() if response == Gtk.ResponseType.OK: filename = dialog.get_filename() save(filename) dialog.destroy() def show_plot_data(self, widget=None, data=None): datawin = DataWindow() d = {} for parameter in self.data_dict: hk, param = parameter.split(':') try: d[hk][param] = self.data_dict[parameter].T except KeyError: d.setdefault(hk, {param: self.data_dict[parameter].T}) hkblocks = [] text = '' for n in d: params = list(d[n].keys()) head = '# {}\n# CUC_Time\t\t'.format(n) + '\t\t'.join(params) + '\n' datablock = '\n'.join(['{:.6F}\t\t'.format(d[n][params[0]][i, 0]) + '\t\t'.join( ['{:.15G}'.format(d[n][param][i, 1]) for param in params]) for i in range(len(d[n][params[0]][:, 1]))]) hkblocks.append(head + datablock) text = '\n\n'.join(hkblocks) buf = datawin.textview.get_buffer() buf.set_text(text) datawin.show_all() # def write_cfg(self, widget=None, dummy=None): # try: # self.cfg.save_to_file() # # except AttributeError: # return def live_plot_off(self, widget, dummy): self.liveplot = False def quit_func(self, *args): # Try to tell terminal in the editor that the variable is not longer availabe for service in dbus.SessionBus().list_names(): if service.startswith(self.cfg['ccs-dbus_names']['editor']): editor = cfl.dbus_connection(service[0:-1].split('.')[1], service[-1]) if self.main_instance == editor.Variables('main_instance'): nr = self.my_bus_name[-1] if nr == str(1): nr = '' editor.Functions('_to_console_via_socket', 'del(paramplot' + str(nr) + ')') self.update_all_connections_quit() Gtk.main_quit() return False def update_all_connections_quit(self): ''' Tells all running applications that it is not longer availabe and suggests another main communicatior if one is available :return: ''' our_con = [] # All connections to running applications without communicions form the same applications as this my_con = [] # All connections to same applications as this for service in dbus.SessionBus().list_names(): if service.split('.')[1] in self.cfg['ccs-dbus_names']: # Check if connection belongs to CCS if service == self.my_bus_name: #If own allplication do nothing continue conn = cfl.dbus_connection(service.split('.')[1], service[-1]) if conn.Variables('main_instance') == self.main_instance: #Check if running in same project if service.startswith(self.my_bus_name[:-1]): #Check if it is same application type my_con.append(service) else: our_con.append(service) instance = my_con[0][-1] if my_con else 0 # Select new main application if possible, is randomly selected our_con = our_con + my_con # Add the instances of same application to change the main communication as well for service in our_con: # Change the main communication for all applications+ conn = cfl.dbus_connection(service.split('.')[1], service[-1]) comm = conn.Functions('get_communication') # Check if this application is the main applications otherwise do nothing if str(comm[self.my_bus_name.split('.')[1]]) == self.my_bus_name[-1]: conn.Functions('change_communication', self.my_bus_name.split('.')[1], instance, False) return def change_communication(self, application, instance=1, check=True): # If it is checked that both run in the same project it is not necessary to do it again if check: conn = cfl.dbus_connection(application, instance) # Both are not in the same project do not change if not conn.Variables('main_instance') == self.main_instance: self.logger.error('Application {} is not in the same project as {}: Can not communicate'.format( self.my_bus_name, self.cfg['ccs-dbus_names'][application] + str(instance))) return cfl.communication[application] = int(instance) return def get_communication(self): return cfl.communication def connect_to_all(self, My_Bus_Name, Count): self.my_bus_name = My_Bus_Name # Look if other applications are running in the same project group our_con = [] # Look for all connections starting with com, therefore only one loop over all connections is necessary for service in dbus.SessionBus().list_names(): if service.startswith('com'): our_con.append(service) # Check if a com connection has the same name as given in cfg file for app in our_con: if app.split('.')[1] in self.cfg['ccs-dbus_names']: # If name is the name of the program skip if app == self.my_bus_name: continue # Otherwise save the main connections in cfl.communication conn_name = app.split('.')[1] conn = cfl.dbus_connection(conn_name, app[-1]) if conn.Variables('main_instance') == self.main_instance: cfl.communication = conn.Functions('get_communication') conn_com = conn.Functions('get_communication') if conn_com[self.my_bus_name.split('.')[1]] == 0: conn.Functions('change_communication', self.my_bus_name.split('.')[1], self.my_bus_name[-1], False) if not cfl.communication[self.my_bus_name.split('.')[1]]: cfl.communication[self.my_bus_name.split('.')[1]] = int(self.my_bus_name[-1]) # Connect to all terminals if Count == 1: for service in dbus.SessionBus().list_names(): if service.startswith(self.cfg['ccs-dbus_names']['editor']): editor = cfl.dbus_connection('editor', service[-1]) editor.Functions('_to_console_via_socket', "paramplot = dbus.SessionBus().get_object('" + str(My_Bus_Name) + "', '/MessageListener')") else: for service in dbus.SessionBus().list_names(): if service.startswith(self.cfg['ccs-dbus_names']['editor']): editor = cfl.dbus_connection('editor', service[-1]) editor.Functions('_to_console_via_socket', "paramplot" + str(Count) + " = dbus.SessionBus().get_object('" + str(My_Bus_Name) + "', '/MessageListener')") # This class seems to be no longer needed # class NavigationToolbarX(NavigationToolbar): # # def __init__(self, *args, **kwargs): # super(NavigationToolbarX, self).__init__(*args, **kwargs) # self._ids_zoom = [] # # # override this function to avoid call to Gtk.main_iteration, # # which causes crash when multiple PlotViewer instances are running # def set_cursor(self, cursor): # # self.canvas.get_property("window").set_cursor(cursord[cursor]) # self.canvas.set_cursor(cursor) # # def release_zoom(self, event): # """the release mouse button callback in zoom to rect mode""" # for zoom_id in self._ids_zoom: # self.canvas.mpl_disconnect(zoom_id) # # self._ids_zoom = [] # # self.remove_rubberband() # # if not self._xypress: # return # # last_a = [] # # for cur_xypress in self._xypress: # x, y = event.x, event.y # lastx, lasty, a, ind, view = cur_xypress # # ignore singular clicks - 5 pixels is a threshold # # allows the user to "cancel" a zoom action # # by zooming by less than 5 pixels # if ((abs(x - lastx) < 5 and self._zoom_mode!="y") or # (abs(y - lasty) < 5 and self._zoom_mode!="x")): # self._xypress = None # self.release(event) # self.draw() # return # # # detect twinx,y axes and avoid double zooming # twinx, twiny = False, False # if last_a: # for la in last_a: # if a.get_shared_x_axes().joined(a, la): # twinx = True # if a.get_shared_y_axes().joined(a, la): # twiny = True # last_a.append(a) # # if self._button_pressed == 1: # direction = 'in' # elif self._button_pressed == 3: # direction = 'out' # else: # continue # # a._set_view_from_bbox((lastx, lasty, x, y), direction, # self._zoom_mode, twinx, twiny) # # xlim, ylim = a.get_xlim(), a.get_ylim() # self.canvas.get_parent().get_parent().get_parent().reduce_datapoints(xlim, ylim) # # self.draw() # self._xypress = None # self._button_pressed = None # # self._zoom_mode = None # # self.push_current() # self.release(event) class DataWindow(Gtk.Window): def __init__(self, parent=None): Gtk.Window.__init__(self) self.set_title('Data Viewer') self.set_default_size(400, 600) sv = Gtk.ScrolledWindow() self.add(sv) self.textview = Gtk.TextView(cursor_visible=False, editable=False) sv.add(self.textview) if __name__ == "__main__": if len(sys.argv) > 1: pool = sys.argv[1] else: print('No pool name specified!') raise TypeError('Must specify pool name') # Important to tell Dbus that Gtk loop can be used before the first dbus command DBusGMainLoop(set_as_default=True) win = PlotViewer(pool) Bus_Name = cfg.get('ccs-dbus_names', 'plotter') # DBusGMainLoop(set_as_default=True) DBus_Basic.MessageListener(win, Bus_Name, *sys.argv) win.connect("delete-event", win.quit_func) win.show_all() Gtk.main()