From 6169709da63cb62ec7b5a5f84a59e80b4154d5d9 Mon Sep 17 00:00:00 2001 From: Gautier Hattenberger Date: Fri, 24 Feb 2023 00:20:05 +0100 Subject: [PATCH] [plotter] implementing a realtime plotter in python (#2997) * [plotter] implementing a realtime plotter in python based on pyqtgraph it should implement most of the functionalities of the legacy ocaml plotter it is replacing the previous implementation in python * Format code. * Add requirements * refactor plotter --------- Co-authored-by: Fabien-B --- conf/tools/real_time_plotter__python_.xml | 2 +- .../python/real_time_plot/messagepicker.py | 138 ------ .../python/real_time_plot/plotframe.py | 255 ---------- .../python/real_time_plot/plotpanel.py | 447 ------------------ .../python/real_time_plot/realtimeplotapp.py | 27 -- .../python/real_time_plot/textdroptarget.py | 13 - sw/logalizer/requirements.txt | 5 + sw/logalizer/rt_plotter.py | 334 +++++++++++++ sw/logalizer/rt_plotter.ui | 302 ++++++++++++ sw/logalizer/ui_rt_plotter.py | 157 ++++++ 10 files changed, 799 insertions(+), 881 deletions(-) delete mode 100755 sw/ground_segment/python/real_time_plot/messagepicker.py delete mode 100644 sw/ground_segment/python/real_time_plot/plotframe.py delete mode 100644 sw/ground_segment/python/real_time_plot/plotpanel.py delete mode 100755 sw/ground_segment/python/real_time_plot/realtimeplotapp.py delete mode 100644 sw/ground_segment/python/real_time_plot/textdroptarget.py create mode 100644 sw/logalizer/requirements.txt create mode 100755 sw/logalizer/rt_plotter.py create mode 100644 sw/logalizer/rt_plotter.ui create mode 100644 sw/logalizer/ui_rt_plotter.py diff --git a/conf/tools/real_time_plotter__python_.xml b/conf/tools/real_time_plotter__python_.xml index 4924fa0751..6e411b9628 100644 --- a/conf/tools/real_time_plotter__python_.xml +++ b/conf/tools/real_time_plotter__python_.xml @@ -1,2 +1,2 @@ - + diff --git a/sw/ground_segment/python/real_time_plot/messagepicker.py b/sw/ground_segment/python/real_time_plot/messagepicker.py deleted file mode 100755 index 4fe9143d29..0000000000 --- a/sw/ground_segment/python/real_time_plot/messagepicker.py +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env python - -from __future__ import absolute_import, print_function - -import wx -import sys -import time -from os import path, getenv - -# if PAPARAZZI_HOME not set, then assume the tree containing this -# file is a reasonable substitute -PPRZ_HOME = getenv("PAPARAZZI_HOME", path.normpath(path.join(path.dirname(path.abspath(__file__)), '../../../../'))) -sys.path.append(PPRZ_HOME + "/var/lib/python") - -from pprzlink.ivy import IvyMessagesInterface -from pprzlink.message import PprzMessage - - -class Message(PprzMessage): - def __init__(self, class_name, name): - super(Message, self).__init__(class_name, name) - self.field_controls = [] - self.index = None - self.last_seen = time.clock() - - -class Aircraft(object): - def __init__(self, ac_id): - self.ac_id = ac_id - self.messages = {} - self.messages_book = None - - -class MessagePicker(wx.Frame): - def __init__(self, parent, callback, ivy_interface=None): - wx.Frame.__init__(self, parent, name="MessagePicker", title=u'Message Picker', size=wx.Size(320,640)) - - self.aircrafts = {} - self.callback = callback - - self.tree = wx.TreeCtrl(self) - self.root = self.tree.AddRoot("Telemetry") - self.tree.Bind(wx.EVT_LEFT_DCLICK, self.OnDoubleClick) - self.tree.Bind(wx.EVT_CHAR, self.OnKeyChar) - self.Bind(wx.EVT_CLOSE, self.OnClose) - if ivy_interface is None: - self.message_interface = IvyMessagesInterface("MessagePicker") - else: - self.message_interface = ivy_interface - self.message_interface.subscribe(self.msg_recv) - - def OnClose(self, event): - # if we have a parent (like the plotpanel) only hide instead of shutdown - if self.GetParent() is not None: - self.Hide() - else: - self.message_interface.shutdown() - self.Destroy() - - def msg_recv(self, ac_id, msg): - if msg.msg_class != "telemetry": - return - - self.tree.Expand(self.root) - if ac_id not in self.aircrafts: - ac_node = self.tree.AppendItem(self.root, str(ac_id)) - self.aircrafts[ac_id] = Aircraft(ac_id) - self.aircrafts[ac_id].messages_book = ac_node - - aircraft = self.aircrafts[ac_id] - ac_node = aircraft.messages_book - - if msg.name not in aircraft.messages: - msg_node = self.tree.AppendItem(ac_node, str(msg.name)) - self.tree.SortChildren(ac_node) - aircraft.messages[msg.name] = Message("telemetry", msg.name) - for field in aircraft.messages[msg.name].fieldnames: - item = self.tree.AppendItem(msg_node, field) - - def OnKeyChar(self, event): - if event.GetKeyCode() != 13: - return False - node = self.tree.GetSelection() - field_name = self.tree.GetItemText(node) - - parent = self.tree.GetItemParent(node) - message_name = self.tree.GetItemText(parent) - - grandparent = self.tree.GetItemParent(parent) - ac_id = self.tree.GetItemText(grandparent) - - if node == self.root or parent == self.root or grandparent == self.root: - # if not leaf, double click = expand - if self.tree.IsExpanded(node): - self.tree.Collapse(node) - else: - self.tree.Expand(node) - return - - self.callback(int(ac_id), message_name, field_name) - - def OnDoubleClick(self, event): - node = self.tree.GetSelection() - field_name = self.tree.GetItemText(node) - - parent = self.tree.GetItemParent(node) - message_name = self.tree.GetItemText(parent) - - grandparent = self.tree.GetItemParent(parent) - ac_id = self.tree.GetItemText(grandparent) - - if node == self.root or parent == self.root or grandparent == self.root: - # if not leaf, double click = expand - if self.tree.IsExpanded(node): - self.tree.Collapse(node) - else: - self.tree.Expand(node) - return - - self.callback(int(ac_id), message_name, field_name) - -class TestApp(wx.App): - def OnInit(self): - self.main = MessagePicker(None, callback) - self.main.Show() - self.SetTopWindow(self.main) - - return True - -def test(): - application = TestApp(0) - application.MainLoop() - -def callback(ac_id, message, field): - print(ac_id, message, field) - -if __name__ == '__main__': - test() diff --git a/sw/ground_segment/python/real_time_plot/plotframe.py b/sw/ground_segment/python/real_time_plot/plotframe.py deleted file mode 100644 index f438db580b..0000000000 --- a/sw/ground_segment/python/real_time_plot/plotframe.py +++ /dev/null @@ -1,255 +0,0 @@ -#Boa:Frame:PlotFrame - -from __future__ import division - -import wx -import plotpanel - - -_INITIAL_TIME_VALUE_ = 0.2 # initial refresh rate in seconds - -def create(parent): - return PlotFrame(parent) - -[wxID_PLOTFRAME, wxID_PLOTFRAMECHECKAUTOSCALE, wxID_PLOTFRAMEEDITMAX, wxID_PLOTFRAMEEDITMIN, wxID_PLOTFRAMEEDITTIME, wxID_PLOTFRAMEPANEL1, wxID_PLOTFRAMESLIDERTIME, wxID_PLOTFRAMESTATICTEXT1, wxID_PLOTFRAMESTATICTEXT2, wxID_PLOTFRAMESTATICTEXT3] = [wx.NewId() for _init_ctrls in range(10)] - -[wxID_PLOTFRAMEMENU1ITEM_ADD, wxID_PLOTFRAMEMENU1ITEM_PAUSE, wxID_PLOTFRAMEMENU1ITEM_RESET] = [wx.NewId() for _init_coll_menuPlot_Items in range(3)] - -class PlotFrame(wx.Frame): - def _init_coll_boxSizer1_Items(self, parent): - # generated method, don't edit - - parent.AddSizer(self.boxSizer2, 0, border=0, flag=0) - parent.AddWindow(self.panel1, 1, border=0, flag=wx.EXPAND) - - def _init_coll_boxSizer2_Items(self, parent): - # generated method, don't edit - - parent.AddWindow(self.checkAutoScale, 0, border=0, flag=wx.ALIGN_CENTER_VERTICAL) - parent.AddWindow(self.staticText1, 0, border=0, flag=wx.ALIGN_CENTER_VERTICAL) - parent.AddWindow(self.editMin, 0, border=0, flag=0) - parent.AddWindow(self.staticText2, 0, border=0, flag=wx.ALIGN_CENTER_VERTICAL) - parent.AddWindow(self.editMax, 0, border=0, flag=0) - parent.AddWindow(self.staticText3, 0, border=0, flag=wx.ALIGN_CENTER_VERTICAL) - parent.AddWindow(self.sliderTime, 0, border=0, flag=wx.ALIGN_CENTER_VERTICAL) - parent.AddWindow(self.editTime, 0, border=0, flag=0) - - def _init_coll_menuBar1_Menus(self, parent): - # generated method, don't edit - - parent.Append(menu=self.menuPlot, title=u'Plot') - parent.Append(menu=self.menuCurves, title=u'Curves') - - def _init_coll_menuPlot_Items(self, parent): - # generated method, don't edit - - parent.Append(help=u'Add plots', id=wxID_PLOTFRAMEMENU1ITEM_ADD, kind=wx.ITEM_NORMAL, text=u'&Add\tCtrl+A') - parent.Append(help=u'Reset plot scale', id=wxID_PLOTFRAMEMENU1ITEM_RESET, kind=wx.ITEM_NORMAL, text=u'&Reset\tCtrl+L') - parent.Append(help=u'Pause the plot', id=wxID_PLOTFRAMEMENU1ITEM_PAUSE, kind=wx.ITEM_CHECK, text=u'&Pause\tCtrl+P') - self.Bind(wx.EVT_MENU, self.OnMenu1Item_addMenu, id=wxID_PLOTFRAMEMENU1ITEM_ADD) - self.Bind(wx.EVT_MENU, self.OnMenu1Item_resetMenu, id=wxID_PLOTFRAMEMENU1ITEM_RESET) - self.Bind(wx.EVT_MENU, self.OnMenu1Item_pauseMenu, id=wxID_PLOTFRAMEMENU1ITEM_PAUSE) - - def _init_sizers(self): - # generated method, don't edit - self.boxSizer1 = wx.BoxSizer(orient=wx.VERTICAL) - - self.boxSizer2 = wx.BoxSizer(orient=wx.HORIZONTAL) - - self._init_coll_boxSizer1_Items(self.boxSizer1) - self._init_coll_boxSizer2_Items(self.boxSizer2) - - self.SetSizer(self.boxSizer1) - - def _init_utils(self): - # generated method, don't edit - self.menuPlot = wx.Menu(title='') - - self.menuCurves = wx.Menu(title='') - - self.menuBar1 = wx.MenuBar() - - self._init_coll_menuPlot_Items(self.menuPlot) - self._init_coll_menuBar1_Menus(self.menuBar1) - - def _init_ctrls(self, prnt): - # generated method, don't edit - wx.Frame.__init__(self, id=wxID_PLOTFRAME, name=u'PlotFrame', parent=prnt, pos=wx.Point(476, 365), size=wx.Size(800, 225), style=wx.DEFAULT_FRAME_STYLE, title=u'Real Time Plot') - self._init_utils() - self.SetMenuBar(self.menuBar1) - self.SetClientSize(wx.Size(800, 225)) - - self.checkAutoScale = wx.CheckBox(id=wxID_PLOTFRAMECHECKAUTOSCALE, label=u'Auto scale', name=u'checkAutoScale', parent=self, pos=wx.Point(0, 2), size=wx.Size(93, 22), style=0) - self.checkAutoScale.SetValue(True) - self.checkAutoScale.Bind(wx.EVT_CHECKBOX, self.OnCheckAutoScaleCheckbox, id=wxID_PLOTFRAMECHECKAUTOSCALE) - - self.staticText1 = wx.StaticText(id=wxID_PLOTFRAMESTATICTEXT1, label=u'min', name='staticText1', parent=self, pos=wx.Point(93, 5), size=wx.Size(68, 17), style=wx.ALIGN_RIGHT) - - self.editMin = wx.TextCtrl(id=wxID_PLOTFRAMEEDITMIN, name=u'editMin', parent=self, pos=wx.Point(161, 0), size=wx.Size(80, 27), style=0, value=u'') - self.editMin.Enable(False) - self.editMin.Bind(wx.EVT_TEXT, self.OnEditMinText, id=wxID_PLOTFRAMEEDITMIN) - - self.staticText2 = wx.StaticText(id=wxID_PLOTFRAMESTATICTEXT2, label=u'max', name='staticText2', parent=self, pos=wx.Point(241, 5), size=wx.Size(68, 17), style=wx.ALIGN_RIGHT) - - self.editMax = wx.TextCtrl(id=wxID_PLOTFRAMEEDITMAX, name=u'editMax', parent=self, pos=wx.Point(309, 0), size=wx.Size(80, 27), style=0, value=u'') - self.editMax.Enable(False) - self.editMax.Bind(wx.EVT_TEXT, self.OnEditMaxText, id=wxID_PLOTFRAMEEDITMAX) - - self.staticText3 = wx.StaticText(id=wxID_PLOTFRAMESTATICTEXT3, label=u'interval', name='staticText3', parent=self, pos=wx.Point(389, 5), size=wx.Size(68, 17), style=wx.ALIGN_RIGHT) - - self.sliderTime = wx.Slider(id=wxID_PLOTFRAMESLIDERTIME, maxValue=1000, minValue=1, name=u'sliderTime', parent=self, pos=wx.Point(457, 4), size=wx.Size(200, 19), style=wx.SL_HORIZONTAL, value=_INITIAL_TIME_VALUE_ * 1000) - self.sliderTime.SetLabel(u'') - self.sliderTime.Bind(wx.EVT_COMMAND_SCROLL, self.OnSliderTimeCommandScroll, id=wxID_PLOTFRAMESLIDERTIME) - - self.editTime = wx.TextCtrl(id=wxID_PLOTFRAMEEDITTIME, name=u'editTime', parent=self, pos=wx.Point(657, 0), size=wx.Size(80, 27), style=wx.TE_PROCESS_ENTER, value="%0.2f" % _INITIAL_TIME_VALUE_) - self.editTime.Bind(wx.EVT_TEXT_ENTER, self.OnEditTimeTextEnter, id=wxID_PLOTFRAMEEDITTIME) - - self.panel1 = wx.Panel(id=wxID_PLOTFRAMEPANEL1, name='panel1', parent=self, pos=wx.Point(0, 27), size=wx.Size(800, 200), style=wx.TAB_TRAVERSAL) - - self._init_sizers() - - def __init__(self, parent): - self._init_ctrls(parent) - - self.canvas = plotpanel.create(self.panel1, self) - self.dynamic_menus = {} - - self.Bind( wx.EVT_CLOSE, self.OnClose) - self.Bind( wx.EVT_ERASE_BACKGROUND, self.OnErase) - self.panel1.Bind( wx.EVT_RIGHT_DOWN, self.OnRightDown) - self.panel1.Bind( wx.EVT_SIZE, self.OnSize) - - def OnRightDown(self, event): - self.PopupMenu(self.menuPlot, event.GetPosition()) - - def AddPlot(self, ac_id, message, field, color = None, x_axis = False): - self.canvas.BindCurve(ac_id, message, field, color, x_axis) - - def SetMinMax(self, min_, max_): - self.editMin.SetValue(str(min_)) - self.editMax.SetValue(str(max_)) - - def OnClose(self, event): - # need to forward close to canvas so that ivy is shut down, otherwise ivy hangs the shutdown - self.canvas.OnClose() - self.Destroy() - - def OnErase(self, event): - pass - - def OnSize(self, event): - self.canvas.OnSize( event.GetSize()) - - def OnSliderTimeCommandScroll(self, event): - value = event.GetPosition() - self.canvas.SetPlotInterval(value) - self.editTime.SetValue( '%.3f' % (value/1000.0)) - - def OnEditTimeTextEnter(self, event): - try: - value = int(float(event.GetString()) * 1000.0) - except: - value = 0 - if value < 1 or value > 1000: - value = '%.3f' % (self.sliderTime.GetValue() / 1000.0) - self.editTime.SetValue( value) - return - self.canvas.SetPlotInterval(value) - self.sliderTime.SetValue(value) - - def OnCheckAutoScaleCheckbox(self, event): - value = self.checkAutoScale.GetValue() - self.editMin.Enable( not value) - self.editMax.Enable( not value) - self.canvas.SetAutoScale(value) - - def OnMenu1Item_addMenu(self, event): - self.canvas.ShowMessagePicker(self) - - def OnMenu1Item_resetMenu(self, event): - self.canvas.ResetScale() - - def OnMenu1Item_pauseMenu(self, event): - self.canvas.Pause(event.IsChecked()) - - def AddCurve(self, menu_id, title, use_as_x = False): - curveMenu = wx.Menu(title='') - - curveMenu.Append(help=u'Delete plot', id=menu_id*10, kind=wx.ITEM_NORMAL, text=u'&Delete') - curveMenu.Append(help=u'Offset plot', id=menu_id*10+1, kind=wx.ITEM_NORMAL, text=u'&Offset') - curveMenu.Append(help=u'Scale plot', id=menu_id*10+2, kind=wx.ITEM_NORMAL, text=u'&Scale') - curveMenu.Append(help=u'Plot data as messages are received rather than async', id=menu_id*10+3, kind=wx.ITEM_CHECK, text=u'&Real time plot') - curveMenu.Append(help=u'Use this curve as the X-axis rather than a time based scale', id=menu_id*10+4, kind=wx.ITEM_CHECK, text=u'&Use as X-axis') - - curveMenu.Check(id=menu_id*10+4, check=bool(use_as_x)) - - self.Bind(wx.EVT_MENU, self.OnMenuDeleteCurve, id=menu_id*10) - self.Bind(wx.EVT_MENU, self.OnMenuOffsetCurve, id=menu_id*10+1) - self.Bind(wx.EVT_MENU, self.OnMenuScaleCurve, id=menu_id*10+2) - self.Bind(wx.EVT_MENU, self.OnMenuRealTime, id=menu_id*10+3) - self.Bind(wx.EVT_MENU, self.OnMenuUseAsXAxis, id=menu_id*10+4) - - self.dynamic_menus[menu_id] = self.menuCurves.AppendSubMenu(submenu=curveMenu, text=title) - - def OnMenuDeleteCurve(self, event): - menu_id = event.GetId() // 10 - item = self.dynamic_menus[menu_id] - self.canvas.RemovePlot(menu_id) - self.menuCurves.DestroyItem(item) - del self.dynamic_menus[menu_id] - - def OnMenuOffsetCurve(self, event): - menu_id = (event.GetId()-1) // 10 - - default_value = str(self.canvas.FindPlot(menu_id).offset) - value = wx.GetTextFromUser("Enter a value to offset the plot", "Offset", default_value) - try: - value = float(value) - self.canvas.OffsetPlot(menu_id, value) - except: - pass - - def OnMenuScaleCurve(self, event): - menu_id = (event.GetId()-2) // 10 - - default_value = str(self.canvas.FindPlot(menu_id).scale) - value = wx.GetTextFromUser("Enter a factor to scale the plot", "Scale", default_value) - try: - value = float(value) - self.canvas.ScalePlot(menu_id, value) - except: - pass - - def OnMenuRealTime(self,event): - menu_id = (event.GetId()-3) // 10 - self.canvas.SetRealTime(menu_id, event.IsChecked()) - - def OnMenuUseAsXAxis(self,event): - event_id = event.GetId() - menu_id = (event_id-4) // 10 - value = event.IsChecked() - - if value: - # go through and clear the checks from any other curves - for i in self.dynamic_menus: - for item in self.dynamic_menus[i].GetSubMenu().GetMenuItems(): - if item.GetText() == u'_Use as X-axis' and event_id != item.GetId(): - item.Check(False) - self.canvas.SetXAxis(menu_id) - else: - self.canvas.ClearXAxis() - - - def OnEditMinText(self, event): - try: - value = float(event.GetString()) - self.canvas.SetMin(value) - except: - pass - - def OnEditMaxText(self, event): - try: - value = float(event.GetString()) - self.canvas.SetMax(value) - except: - pass diff --git a/sw/ground_segment/python/real_time_plot/plotpanel.py b/sw/ground_segment/python/real_time_plot/plotpanel.py deleted file mode 100644 index 871f4c0d39..0000000000 --- a/sw/ground_segment/python/real_time_plot/plotpanel.py +++ /dev/null @@ -1,447 +0,0 @@ -from __future__ import absolute_import, print_function, division - -import wx -from ivy.std_api import * -from ivy.ivy import IvyIllegalStateError -import logging -from textdroptarget import * -import math -import random -import sys -from os import getenv, path -import messagepicker - -# if PAPARAZZI_SRC or PAPARAZZI_HOME not set, then assume the tree containing this -# file is a reasonable substitute -PPRZ_HOME = getenv("PAPARAZZI_HOME", path.normpath(path.join(path.dirname(path.abspath(__file__)), '../../../../'))) -PPRZ_SRC = getenv("PAPARAZZI_SRC", path.normpath(path.join(path.dirname(path.abspath(__file__)), '../../../../'))) -sys.path.append(PPRZ_SRC + "/sw/lib/python") -sys.path.append(PPRZ_HOME + "/var/lib/python") # pprzlink - -import pprz_env -from pprzlink import messages_xml_map -from pprzlink.ivy import IvyMessagesInterface -from pprzlink.message import PprzMessage - -class PlotData: - def __init__(self, ivy_msg_id, title, width, color=None, scale=1.0): - self.id = ivy_msg_id - self.title = title - self.SetPlotSize(width) - self.x_min = 1e32 - self.x_max = 1e-32 - - self.avg = 0.0 - self.std_dev = 0.0 - self.real_time = False - - self.scale = scale - self.offset = 0.0 - - if color is not None: - self.color = color - else: - r, g, b = random.randint(0, 255), random.randint(0, 255), random.randint(0, 255) - self.color = wx.Colour(r, g, b) - - def SetRealTime(self, value): - self.real_time = value - - def SetOffset(self, value): - self.offset = value - - def SetScale(self, value): - self.scale = value - - def SetPlotSize(self, size): - self.size = size - self.index = size - 1 # holds the index of the next point to add and the first point to draw - self.data = [] # holds the list of points to plot - for i in range(size): - self.data.append(None) - - self.avg = 0.0 - self.std_dev = 0.0 - - def AddPoint(self, point, x_axis): - self.data[self.index] = point - - if self.real_time or (x_axis is not None): - self.index = (self.index + 1) % self.size # increment index to next point - self.data[self.index] = None - - def DrawTitle(self, dc, margin, width, height): - - text = 'avg:%.2f std:%.2f %s' % (self.avg, self.std_dev, self.title) - - (w, h) = dc.GetTextExtent(text) - dc.SetBrush(wx.Brush(self.color)) - dc.DrawRectangle(width - h - margin, height, h, h) - dc.DrawText(text, width - 2 * margin - w - h, height) - return h - - def DrawCurve(self, dc, width, height, margin, _max_, _min_, x_axis): - if width != self.size: - self.SetPlotSize(width) - return - - if (not self.real_time) and (x_axis is None): - self.index = (self.index + 1) % self.size # increment index to next point - self.data[self.index] = None - - if x_axis is not None: - (x_min, x_max) = x_axis.GetXMinMax() - - dc.SetPen(wx.Pen(self.color, 1)) - if _max_ < _min_: - (_min_, _max_) = (-1, 1) # prevent divide by zero or inversion - if _max_ == _min_: - (_min_, _max_) = (_max_ - 0.5, _max_ + 0.5) - delta = _max_ - _min_ - dy = (height - margin * 2) / delta - - n = 0 - sums = 0.0 - sum_squares = 0.0 - lines = [] - point_1 = None - for i in range(self.size): - ix = (i + self.index) % self.size - point = self.data[ix] - if point is None: - continue - n += 1 - sums = sums + point - sum_squares = sum_squares + (point * point) - - if x_axis is not None: - x = x_axis.data[ix] - if x is None: - continue - dx = (width - 1) / (x_max - x_min) - x = int((x - x_min) * dx) - - else: - x = i * width / self.size - - scaled_point = (point + self.offset) * self.scale - y = height - margin - int((scaled_point - _min_) * dy) - - if point_1 is not None: - line = (point_1[0], point_1[1], x, y) - lines.append(line) - point_1 = (x, y) - dc.DrawLineList(lines) - - if n > 0: - self.avg = sums / n - self.std_dev = math.sqrt(math.fabs((sum_squares / n) - (self.avg * self.avg))) - - def GetXMinMax(self): - x_min = 1e32 - x_max = -1e32 - - for i in range(self.size): - point = self.data[i] - if point is None: - continue - x_min = min(x_min, point) - x_max = max(x_max, point) - - if x_max < x_min: - (x_min, x_max) = (-1, 1) # prevent divide by zero or inversion - if x_max == x_min: - (x_min, x_max) = (x_max - 0.5, x_max + 0.5) - - self.x_max = x_max - self.x_min = x_min - return (x_min, x_max) - - -_IVY_APPNAME = 'RealtimePlot' -_IVY_STRING = '(%s %s .*$)' - - -# _IVY_STRING = '^([^ ]*) +(%s( .*|$))' ## <-- from original ocaml (doesn't work here, just returns Sender field...) - -def create(parent, frame): - return PlotPanel(parent, frame) - - -class PlotPanel(object): - def __init__(self, parent, frame): - self.parent = parent # we are drawing on our parent, so dc comes from this - self.frame = frame # the frame owns any controls we might need to update - - parent.SetDropTarget(TextDropTarget(self)) # calls self.OnDropText when drag and drop complete - - self.width = 800 - self.height = 200 - self.margin = min(self.height / 10, 20) - self.font = wx.Font(self.margin / 2, wx.DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) - self.pixmap = wx.EmptyBitmap(self.width, self.height) - self.plot_size = self.width - self.max = -1e32 - self.min = 1e32 - self.plot_interval = 200 - self.plots = {} - self.auto_scale = True - self.offset = 0.0 - self.scale = 1.0 - self.x_axis = None - - messages_xml_map.parse_messages() - - self.ivy_interface = IvyMessagesInterface(_IVY_APPNAME) - - # start the timer - self.timer = wx.FutureCall(self.plot_interval, self.OnTimer) - - def SetPlotInterval(self, value): - self.plot_interval = value - self.timer.Restart(self.plot_interval) - self.timer = wx.FutureCall(self.plot_interval, self.OnTimer) - - def SetAutoScale(self, value): - self.auto_scale = value - - def SetMin(self, value): - self.min = value - - def SetMax(self, value): - self.max = value - - def Pause(self, pause): - if pause: - self.timer.Stop() - else: - self.timer = wx.FutureCall(self.plot_interval, self.OnTimer) - - def ResetScale(self): - self.max = -1e32 - self.min = 1e32 - - def OnClose(self): - self.timer.Stop() - try: - IvyStop() - except IvyIllegalStateError as e: - print(e) - - def OnErase(self, event): - pass - - def ShowMessagePicker(self, parent): - frame = messagepicker.MessagePicker(parent, self.BindCurve, self.ivy_interface) - frame.Show() - - def OnDropText(self, data): - [ac_id, category, message, field, scale] = data.encode('ASCII').split(':') - self.BindCurve(int(ac_id), message, field, scale=float(scale)) - - def OnIvyMsg(self, agent, *larg): - # print(larg[0]) - data = larg[0].split(' ') - ac_id = int(data[0]) - message = data[1] - - if ac_id not in self.plots: - return - - if message not in self.plots[ac_id]: - return - - for field in self.plots[ac_id][message]: - plot = self.plots[ac_id][message][field] - ix = messages_xml_map.message_dictionary["telemetry"][message].index(field) - point = float(data[ix + 2]) - - if self.x_axis is None or self.x_axis.id != plot.id: - if self.auto_scale: - scaled_point = (point + plot.offset) * plot.scale - self.max = max(self.max, scaled_point) - self.min = min(self.min, scaled_point) - - if self.x_axis is not None: - plot.index = self.x_axis.index - plot.AddPoint(point, self.x_axis) - - def BindCurve(self, ac_id, message, field, color=None, use_as_x=False, scale=1.0): - # -- add this telemetry to our list of things to plot ... - message_string = _IVY_STRING % (ac_id, message) - # print('Binding to %s' % message_string) - - if ac_id not in self.plots: - self.plots[ac_id] = {} - - if message not in self.plots[ac_id]: - self.plots[ac_id][message] = {} - - if field in self.plots[ac_id][message]: - self.plots[ac_id][message][field].color = wx.Color(random.randint(0, 255), random.randint(0, 255), - random.randint(0, 255)) - return - - ivy_id = self.ivy_interface.bind_raw(self.OnIvyMsg, str(message_string)) - title = '%i:%s:%s' % (ac_id, message, field) - self.plots[ac_id][message][field] = PlotData(ivy_id, title, self.plot_size, color, scale) - self.frame.AddCurve(ivy_id, title, use_as_x) - if use_as_x: - self.x_axis = self.plots[ac_id][message][field] - - def CalcMinMax(self, plot): - if not self.auto_scale: return - for x in plot.data: - self.max = max(self.max, x) - self.min = min(self.min, x) - self.frame.SetMinMax(self.min, self.max) - - def FindPlotName(self, ivy_id): - for ac_id in self.plots: - for msg in self.plots[ac_id]: - for field in self.plots[ac_id][msg]: - if self.plots[ac_id][msg][field].id == ivy_id: - return (ac_id, msg, field) - return (None, None, None) - - def FindPlot(self, ivy_id): - (ac_id, msg, field) = self.FindPlotName(ivy_id) - if ac_id is None: - return None - - return self.plots[ac_id][msg][field] - - def RemovePlot(self, ivy_id): - (ac_id, msg, field) = self.FindPlotName(ivy_id) - if ac_id is None: - return - - if (self.x_axis is not None) and (self.x_axis.id == ivy_id): - self.x_axis = None - - self.ivy_interface.unbind(ivy_id) - del self.plots[ac_id][msg][field] - if len(self.plots[ac_id][msg]) == 0: - del self.plots[ac_id][msg] - - def OffsetPlot(self, ivy_id, offset): - plot = self.FindPlot(ivy_id) - if plot is None: - return - - plot.SetOffset(offset) - print('panel value: %.2f' % value) - CalcMinMax(plot) - - def ScalePlot(self, ivy_id, offset): - plot = self.FindPlot(ivy_id) - if plot is None: - return - - plot.SetScale(offset) - CalcMinMax(plot) - - def SetRealTime(self, ivy_id, value): - plot = self.FindPlot(ivy_id) - if plot is None: - return - - plot.SetRealTime(value) - - def SetXAxis(self, ivy_id): - plot = self.FindPlot(ivy_id) - if plot is None: - return - - self.x_axis = plot - - def ClearXAxis(self): - self.x_axis = None - - def OnSize(self, size): - (width, height) = size - if self.width == width and self.height == height: - return - - self.pixmap = wx.EmptyBitmap(width, height) - self.width = width - self.height = height - self.plot_size = width - self.margin = min(self.height / 10, 20) - self.font = wx.Font(self.margin / 2, wx.DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) - - def OnTimer(self): - self.timer.Restart(self.plot_interval) - self.frame.SetMinMax(self.min, self.max) - self.DrawFrame() - - def DrawFrame(self): - dc = wx.ClientDC(self.parent) - bdc = wx.BufferedDC(dc, self.pixmap) - bdc.SetBackground(wx.Brush("White")) - bdc.Clear() - - self.DrawBackground(bdc, self.width, self.height) - - title_y = 2 - for ac_id in self.plots: - for message in self.plots[ac_id]: - for field in self.plots[ac_id][message]: - plot = self.plots[ac_id][message][field] - if (self.x_axis is not None) and (self.x_axis.id == plot.id): - continue - title_height = plot.DrawTitle(bdc, 2, self.width, title_y) - plot.DrawCurve(bdc, self.width, self.height, self.margin, self.max, self.min, self.x_axis) - - title_y += title_height + 2 - - def DrawBackground(self, dc, width, height): - - # Time Graduations - dc.SetFont(self.font) - - if self.x_axis is None: - t = self.plot_interval * width - t1 = "0.0s" - t2 = "-%.1fs" % (t / 2000.0) - t3 = "-%.1fs" % (t / 1000.0) - else: - x_max = self.x_axis.x_max - x_min = self.x_axis.x_min - t1 = "%.2f" % x_max - t2 = "%.2f" % (x_min + (x_max - x_min) / 2.0) - t3 = "%.2f" % x_min - - (w, h) = dc.GetTextExtent(t1) - dc.DrawText(t1, width - w, height - h) - # (w,h) = dc.GetTextExtent(t2) #save time since h will be the same - dc.DrawText(t2, width / 2, height - h) - # (w,h) = dc.GetTextExtent(t3) #save time since h will be the same - dc.DrawText(t3, 0, height - h) - - # Y graduations - if self.max == -1e32: - return - - (_min_, _max_) = (self.min, self.max) - if _max_ < _min_: # prevent divide by zero or inversion - (_min_, _max_) = (-1, 1) - if _max_ == _min_: - (_min_, _max_) = (_max_ - 0.5, _max_ + 0.5) - - delta = _max_ - _min_ - dy = (height - self.margin * 2) / delta - scale = math.log10(delta) - d = math.pow(10.0, math.floor(scale)) - u = d - if delta < 2 * d: - u = d / 5 - elif delta < 5 * d: - u = d / 2 - tick_min = _min_ - math.fmod(_min_, u) - for i in range(int(delta / u) + 1): - tick = tick_min + float(i) * u - s = str(tick) - (w, h) = dc.GetTextExtent(s) - y = height - self.margin - int((tick - _min_) * dy) - h / 2 - dc.DrawText(s, 0, y) diff --git a/sw/ground_segment/python/real_time_plot/realtimeplotapp.py b/sw/ground_segment/python/real_time_plot/realtimeplotapp.py deleted file mode 100755 index 2b4df99aa5..0000000000 --- a/sw/ground_segment/python/real_time_plot/realtimeplotapp.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python - -import wx -import getopt -import sys -import plotframe - - -class RealTimePlotApp(wx.App): - def OnInit(self): - self.main = plotframe.create(None) - self.main.Show() - self.SetTopWindow(self.main) - opts, args = getopt.getopt(sys.argv[1:], "p:", ["plot"]) - for o, a in opts: - if o in ("-p", "--plot"): - [ac_id, message, field, color, use_x] = a.split(':') - self.main.AddPlot(int(ac_id), message, field, color, bool(int(use_x))) - return True - - -def main(): - application = RealTimePlotApp(0) - application.MainLoop() - -if __name__ == '__main__': - main() diff --git a/sw/ground_segment/python/real_time_plot/textdroptarget.py b/sw/ground_segment/python/real_time_plot/textdroptarget.py deleted file mode 100644 index 28d3cb2551..0000000000 --- a/sw/ground_segment/python/real_time_plot/textdroptarget.py +++ /dev/null @@ -1,13 +0,0 @@ -import wx - -class TextDropTarget(wx.TextDropTarget): - """ This object implements Drop Target functionality for Text """ - def __init__(self, reference): - """ Initialize the Drop Target, passing in the Object Reference to - indicate what should receive the dropped text """ - wx.TextDropTarget.__init__(self) - self.reference = reference - - def OnDropText(self, x, y, data): - """ When text is dropped, send it to the object specified """ - self.reference.OnDropText(data) diff --git a/sw/logalizer/requirements.txt b/sw/logalizer/requirements.txt new file mode 100644 index 0000000000..899686c3fc --- /dev/null +++ b/sw/logalizer/requirements.txt @@ -0,0 +1,5 @@ +PyQt5 +pyqtgraph +numpy +ivy-python + diff --git a/sw/logalizer/rt_plotter.py b/sw/logalizer/rt_plotter.py new file mode 100755 index 0000000000..dc4141e8b6 --- /dev/null +++ b/sw/logalizer/rt_plotter.py @@ -0,0 +1,334 @@ +#!/usr/bin/python3 +import sys +import signal +import re +from os import path, getenv +from PyQt5 import QtCore, QtWidgets, QtGui +from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QAction, QToolButton, QMenu +import argparse +from ui_rt_plotter import Ui_RT_Plotter +from time import sleep +import numpy as np +import pyqtgraph as pg +from dataclasses import dataclass, Field, field +from typing import List, Dict, Optional + +# if PAPARAZZI_SRC or PAPARAZZI_HOME not set, then assume the tree containing this +# file is a reasonable substitute +PPRZ_HOME = getenv("PAPARAZZI_HOME", path.normpath(path.join(path.dirname(path.abspath(__file__)), '../../../../'))) +PPRZ_SRC = getenv("PAPARAZZI_SRC", path.normpath(path.join(path.dirname(path.abspath(__file__)), '../../../../'))) + +sys.path.append(PPRZ_SRC + "/sw/lib/python") +sys.path.append(PPRZ_HOME + "/var/lib/python") # pprzlink + +from pprzlink.ivy import IvyMessagesInterface +from pprzlink.message import PprzMessage + + +@dataclass() +class Curve: + field: str + scale: float + offset: float + nb: int + action: QAction + time: np.ndarray = field(default=None) + data: np.ndarray = field(default=None) + last_data: Optional[float] = field(default=None) + plot: pg.PlotDataItem = field(default=None) + bind: Field = field(default=None) + + +@dataclass() +class Constant: + value: float + action: QAction + plot: pg.PlotDataItem + + +class CurvesCollection: + def __init__(self): + self.data = {} # type: Dict[str, Dict[str, Dict[str, Curve]]] ## {ac_id:{ msg_name:{key:Curve}}} + + def curve_exists(self, ac_id, msg_name, key) -> bool: + try: + c = self.get_curve(ac_id, msg_name, key) + return True + except KeyError: + return False + + def get_curve(self, ac_id, msg_name, key) -> Curve: + """Throw KeyError if this curve does not exists""" + return self.data[ac_id][msg_name][key] + + def remove_curve(self, ac_id, msg_name, key): + """Throw KeyError if this curve does not exists""" + del self.data[ac_id][msg_name][key] + + def add_curve(self, ac_id, msg_name, key, c): + dam = self.data.setdefault(ac_id, {}).setdefault(msg_name, {}) + dam[key] = c + + def get_all_curves(self) -> List[Curve]: + all_curves = [] + for ac_id in self.data: + for msg_name in self.data[ac_id]: + for key in self.data[ac_id][msg_name]: + c = self.data[ac_id][msg_name][key] + all_curves.append(c) + return all_curves + + def curves(self, ac_id, msg_name) -> List[Curve]: + return list(self.data.get(ac_id, {}).get(msg_name, {}).values()) + + +class Plotter(QWidget, Ui_RT_Plotter): + """ + Main plotter class + """ + + def __init__(self, parent, ivy): + QWidget.__init__(self, parent=parent) + self.setupUi(self) + self.plot = pg.PlotWidget() + self.plot.setAcceptDrops(True) + self.plot.dragMoveEvent = self.dragMoveEvent + self.plot.dragEnterEvent = self.dragEnterEvent + self.plot.dragLeaveEvent = self.dragLeaveEvent + self.plot.dropEvent = self.dropEvent + self.plot.setRange(xRange=(-30.0, 0.)) + self.plot.disableAutoRange(pg.ViewBox.XAxis) + self.plot.setMouseEnabled(x=False, y=True) + self.plot.addLegend(offset=(1, 1)) + self.plot.showGrid(x=True, y=True) + self.plot.setBackground('w') + # self.plot.setLabel('bottom','time (s)') + self.gridLayout.addWidget(self.plot, 1, 0, 1, 14) + self.menu_button.setPopupMode(QToolButton.InstantPopup) + self.menu = QMenu(self.menu_button) + self.menu.addAction(self.action_new_plot) + self.menu.addAction(self.action_reset) + self.menu.addAction(self.action_suspend) + self.menu.addAction(self.action_restart) + self.menu.addAction(self.action_dark_background) + self.menu.addSeparator() + self.menu.addAction(self.action_close) + self.menu.addAction(self.action_quit) + self.menu.addSeparator() + self.menu_button.setMenu(self.menu) + self.action_new_plot.triggered.connect(self.open_new_plotter) + self.action_reset.triggered.connect(self.reset_plotter) + self.action_suspend.triggered.connect(self.suspend_plotter) + self.action_restart.triggered.connect(self.restart_plotter) + self.action_dark_background.triggered.connect(self.set_background) + self.action_close.triggered.connect(self.close_window) + self.action_quit.triggered.connect(self.quit_plotter) + self.constant_input.returnPressed.connect(self.draw_constant) + self.ivy = ivy + self.pattern = re.compile("^([0-9]+):(\\w+):(\\w+):(\\w+):([0-9]+.[0-9]*).*$") + self.data = CurvesCollection() + self.constants = {} # type: Dict[float, Constant] + self.dt_slider.valueChanged.connect(self.set_dt) + self.size_slider.valueChanged.connect(self.set_size) + self.autoscale.clicked.connect(lambda: self.plot.enableAutoRange(x=False, y=True)) + self.timer = QtCore.QTimer() + self.timer.setInterval(self.dt_slider.value() * 10) + self.timer.timeout.connect(self.update_plot_data) + self.timer.start() + self.suspended = False + self.nb_color = 8 + self.idx_color = 0 + # max display size (s) / min update time (s) (with rounding) + self.max_size = int(self.size_slider.maximum() / (self.dt_slider.minimum() / 100) + 0.5) + + def get_dt(self): + return self.dt_slider.value() / 100. + + def set_dt(self): + self.dt_label.setText("{:1.2f} s".format(self.get_dt())) + self.timer.setInterval(int(self.dt_slider.value() * 10)) + + def set_size(self): + size = self.size_slider.value() + self.size_label.setText("{} s".format(size)) + self.plot.setRange(xRange=(-size, 0.)) + + def update_plot_data(self): + for curve in self.data.get_all_curves(): + x = curve.time - self.get_dt() + y = curve.data + if curve.last_data is not None: + x = np.roll(x, -1) + x[-1] = 0. + y = np.roll(y, -1) + y[-1] = curve.scale * curve.last_data + curve.offset + curve.nb = min(curve.nb + 1, self.max_size) + curve.last_data = None + curve.data = y + curve.time = x + if not self.suspended: + curve.plot.setData(x[-curve.nb:], y[-curve.nb:]) + + def closing(self): + """ shutdown Ivy and window """ + pass + + def dragLeaveEvent(self, e): + pass + + def dragMoveEvent(self, e): + e.accept() + + def dragEnterEvent(self, e): + if e.mimeData().hasFormat('text/plain'): + e.accept() + else: + e.ignore() + + def dropEvent(self, event): + event.accept() + match = self.pattern.fullmatch(event.mimeData().text()) + if match is None: + return + ac_id, class_name, msg_name, field, sc = match.group(1, 2, 3, 4, 5) + scale = float(sc) * self.scale_spin.value() + offset = self.offset_spin.value() + key = '{}:{}+{}'.format(field, scale, offset) + + if self.data.curve_exists(ac_id, msg_name, key): + return + + menu_item = QAction("remove {}".format(match.group(0))) + plot = self.plot.plot([], [], pen=(self.idx_color, self.nb_color), + name='{}:{}:{}:{}'.format(ac_id, class_name, msg_name, key)) + bind = ivy.subscribe(self.msg_callback, '^({} {} .*)'.format(ac_id, msg_name)) + + c = Curve(field=field, + data=np.zeros(self.max_size), + last_data=None, + time=np.zeros(self.max_size), + scale=scale, + offset=offset, + nb=0, + action=menu_item, + plot=plot, + bind=bind) + + self.data.add_curve(ac_id, msg_name, key, c) + + self.idx_color = (self.idx_color + 1) % self.nb_color + self.menu.addAction(menu_item) + menu_item.triggered.connect(lambda: self.remove_curve(ac_id, msg_name, key)) + + def msg_callback(self, ac_id, msg): + for c in self.data.curves(str(ac_id), msg.name): + c.last_data = float(msg[c.field]) + + def remove_curve(self, ac_id, msg_name, key): + curve = self.data.get_curve(ac_id, msg_name, key) + self.ivy.unbind(curve.bind) + plot_id = curve.plot + self.data.remove_curve(ac_id, msg_name, key) + self.plot.removeItem(plot_id) + + def open_new_plotter(self): + self.parent().open_new_window() + + def reset_plotter(self): + for c in self.data.get_all_curves(): + c.nb = 0 + + def suspend_plotter(self): + self.suspended = True + + def restart_plotter(self): + self.suspended = False + + def close_window(self): + self.parent().close() + + def quit_plotter(self): + QApplication.exit() + + def set_background(self, dark): + if dark: + self.plot.setBackground('k') + else: + self.plot.setBackground('w') + + def draw_constant(self): + try: + value = float(self.constant_input.text()) + if value not in self.constants: + action = QAction('remove {}'.format(value)) + self.menu.addAction(action) + plot = self.plot.plot([-self.size_slider.maximum(), 0], [value, value], + name='constant {}'.format(value), pen='b') + c = Constant(value=value, action=action, plot=plot) + action.triggered.connect(lambda: self.remove_constant(c)) + self.constants[value] = c + + except ValueError: + print("Input constant value is not a number") + + def remove_constant(self, c: Constant): + self.plot.removeItem(c.plot) + self.menu.removeAction(c.action) + del self.constants[c.value] + + + +class MainWindow(QMainWindow): + + def __init__(self, ivy, new_window): + super().__init__() + icon = QtGui.QIcon(path.join(PPRZ_HOME, "data", "pictures", "penguin_icon_rtp.png")) + self.setWindowIcon(icon) + self.setGeometry(0, 0, 900, 300) + self.setMinimumSize(600, 200) + self.ivy = ivy + self.new_window = new_window + self.plotter = Plotter(self, ivy) + self.setCentralWidget(self.plotter) + + def close_plotter(self): + self.plotter.closing() + + def open_new_window(self): + self.new_window() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description="Real-time plotter") + + try: + app = QtWidgets.QApplication(sys.argv) + ivy = IvyMessagesInterface("natnet2ivy") + windows = [] + + + def make_new_window(): + w = MainWindow(ivy, make_new_window) + windows.append(w) + w.show() + + + def closing(): + ivy.shutdown() + for w in windows: + w.close_plotter() + + + def sigint_handler(*args): + """Handler for the SIGINT signal.""" + app.quit() + + + app.aboutToQuit.connect(closing) + signal.signal(signal.SIGINT, sigint_handler) + make_new_window() + sys.exit(app.exec_()) + + except Exception as e: + print('exit on exception:', e) + ivy.shutdown() diff --git a/sw/logalizer/rt_plotter.ui b/sw/logalizer/rt_plotter.ui new file mode 100644 index 0000000000..31ab6a0ed2 --- /dev/null +++ b/sw/logalizer/rt_plotter.ui @@ -0,0 +1,302 @@ + + + RT_Plotter + + + + 0 + 0 + 910 + 404 + + + + + 0 + 0 + + + + false + + + Real-time plotter + + + + + + + + update + + + + + + + + 0 + 0 + + + + 0.50 s + + + 5 + + + + + + + 30 s + + + 3 + + + + + + + Qt::ActionsContextMenu + + + ... + + + + + + + + + + size + + + + + + + + 0 + 0 + + + + scale next by + + + 0 + + + + + + + update time (s) + + + 5 + + + 100 + + + 50 + + + Qt::Horizontal + + + + + + + constant + + + + + + + display time interval + + + 10 + + + 240 + + + 30 + + + 30 + + + Qt::Horizontal + + + + + + + scaling factor + + + -9999.000000000000000 + + + 9999.989999999999782 + + + 1.000000000000000 + + + + + + + restart autoscale on Y axis + + + auto scale + + + + + + + + 0 + 0 + + + + + 45 + 0 + + + + enter a number and press enter to draw a line + + + + + + + offset + + + -9999.000000000000000 + + + 9999.989999999999782 + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + + + New plot + + + Ctrl+N + + + + + Reset + + + reset data for all plots + + + Ctrl+L + + + + + Suspend + + + freeze plotter view + + + Ctrl+S + + + + + Restart + + + restart suspended plotter + + + Ctrl+X + + + + + Close + + + close plotter window + + + Ctrl+W + + + + + Quit + + + quit realtime plotter (close all windows) + + + Ctrl+Q + + + + + true + + + Dark background + + + change background from white to black + + + Ctrl+B + + + + + + diff --git a/sw/logalizer/ui_rt_plotter.py b/sw/logalizer/ui_rt_plotter.py new file mode 100644 index 0000000000..2ca59cfe19 --- /dev/null +++ b/sw/logalizer/ui_rt_plotter.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'rt_plotter.ui' +# +# Created by: PyQt5 UI code generator 5.14.1 +# +# WARNING! All changes made in this file will be lost! + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_RT_Plotter(object): + def setupUi(self, RT_Plotter): + RT_Plotter.setObjectName("RT_Plotter") + RT_Plotter.resize(910, 404) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(RT_Plotter.sizePolicy().hasHeightForWidth()) + RT_Plotter.setSizePolicy(sizePolicy) + RT_Plotter.setAcceptDrops(False) + self.verticalLayout = QtWidgets.QVBoxLayout(RT_Plotter) + self.verticalLayout.setObjectName("verticalLayout") + self.gridLayout = QtWidgets.QGridLayout() + self.gridLayout.setObjectName("gridLayout") + self.label_3 = QtWidgets.QLabel(RT_Plotter) + self.label_3.setObjectName("label_3") + self.gridLayout.addWidget(self.label_3, 0, 1, 1, 1) + self.dt_label = QtWidgets.QLabel(RT_Plotter) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.dt_label.sizePolicy().hasHeightForWidth()) + self.dt_label.setSizePolicy(sizePolicy) + self.dt_label.setObjectName("dt_label") + self.gridLayout.addWidget(self.dt_label, 0, 2, 1, 1) + self.size_label = QtWidgets.QLabel(RT_Plotter) + self.size_label.setObjectName("size_label") + self.gridLayout.addWidget(self.size_label, 0, 5, 1, 1) + self.menu_button = QtWidgets.QToolButton(RT_Plotter) + self.menu_button.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu) + icon = QtGui.QIcon.fromTheme("format-justify-fill") + self.menu_button.setIcon(icon) + self.menu_button.setObjectName("menu_button") + self.gridLayout.addWidget(self.menu_button, 0, 13, 1, 1) + self.label_5 = QtWidgets.QLabel(RT_Plotter) + self.label_5.setObjectName("label_5") + self.gridLayout.addWidget(self.label_5, 0, 4, 1, 1) + self.label_2 = QtWidgets.QLabel(RT_Plotter) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label_2.sizePolicy().hasHeightForWidth()) + self.label_2.setSizePolicy(sizePolicy) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 0, 10, 1, 1) + self.dt_slider = QtWidgets.QSlider(RT_Plotter) + self.dt_slider.setMinimum(5) + self.dt_slider.setMaximum(100) + self.dt_slider.setSliderPosition(50) + self.dt_slider.setOrientation(QtCore.Qt.Horizontal) + self.dt_slider.setObjectName("dt_slider") + self.gridLayout.addWidget(self.dt_slider, 0, 3, 1, 1) + self.label = QtWidgets.QLabel(RT_Plotter) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 0, 7, 1, 1) + self.size_slider = QtWidgets.QSlider(RT_Plotter) + self.size_slider.setMinimum(10) + self.size_slider.setMaximum(240) + self.size_slider.setProperty("value", 30) + self.size_slider.setSliderPosition(30) + self.size_slider.setOrientation(QtCore.Qt.Horizontal) + self.size_slider.setObjectName("size_slider") + self.gridLayout.addWidget(self.size_slider, 0, 6, 1, 1) + self.scale_spin = QtWidgets.QDoubleSpinBox(RT_Plotter) + self.scale_spin.setMinimum(-9999.0) + self.scale_spin.setMaximum(9999.99) + self.scale_spin.setProperty("value", 1.0) + self.scale_spin.setObjectName("scale_spin") + self.gridLayout.addWidget(self.scale_spin, 0, 11, 1, 1) + self.autoscale = QtWidgets.QPushButton(RT_Plotter) + self.autoscale.setObjectName("autoscale") + self.gridLayout.addWidget(self.autoscale, 0, 0, 1, 1) + self.constant_input = QtWidgets.QLineEdit(RT_Plotter) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.constant_input.sizePolicy().hasHeightForWidth()) + self.constant_input.setSizePolicy(sizePolicy) + self.constant_input.setMinimumSize(QtCore.QSize(45, 0)) + self.constant_input.setObjectName("constant_input") + self.gridLayout.addWidget(self.constant_input, 0, 8, 1, 1) + self.offset_spin = QtWidgets.QDoubleSpinBox(RT_Plotter) + self.offset_spin.setMinimum(-9999.0) + self.offset_spin.setMaximum(9999.99) + self.offset_spin.setObjectName("offset_spin") + self.gridLayout.addWidget(self.offset_spin, 0, 12, 1, 1) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) + self.gridLayout.addItem(spacerItem, 0, 9, 1, 1) + self.verticalLayout.addLayout(self.gridLayout) + self.action_new_plot = QtWidgets.QAction(RT_Plotter) + self.action_new_plot.setObjectName("action_new_plot") + self.action_reset = QtWidgets.QAction(RT_Plotter) + self.action_reset.setObjectName("action_reset") + self.action_suspend = QtWidgets.QAction(RT_Plotter) + self.action_suspend.setObjectName("action_suspend") + self.action_restart = QtWidgets.QAction(RT_Plotter) + self.action_restart.setObjectName("action_restart") + self.action_close = QtWidgets.QAction(RT_Plotter) + self.action_close.setObjectName("action_close") + self.action_quit = QtWidgets.QAction(RT_Plotter) + self.action_quit.setObjectName("action_quit") + self.action_dark_background = QtWidgets.QAction(RT_Plotter) + self.action_dark_background.setCheckable(True) + self.action_dark_background.setObjectName("action_dark_background") + + self.retranslateUi(RT_Plotter) + QtCore.QMetaObject.connectSlotsByName(RT_Plotter) + + def retranslateUi(self, RT_Plotter): + _translate = QtCore.QCoreApplication.translate + RT_Plotter.setWindowTitle(_translate("RT_Plotter", "Real-time plotter")) + self.label_3.setText(_translate("RT_Plotter", "update")) + self.dt_label.setText(_translate("RT_Plotter", "0.50 s")) + self.size_label.setText(_translate("RT_Plotter", "30 s")) + self.menu_button.setText(_translate("RT_Plotter", "...")) + self.label_5.setText(_translate("RT_Plotter", "size")) + self.label_2.setText(_translate("RT_Plotter", "scale next by")) + self.dt_slider.setToolTip(_translate("RT_Plotter", "update time (s)")) + self.label.setText(_translate("RT_Plotter", "constant")) + self.size_slider.setToolTip(_translate("RT_Plotter", "display time interval")) + self.scale_spin.setToolTip(_translate("RT_Plotter", "scaling factor")) + self.autoscale.setToolTip(_translate("RT_Plotter", "restart autoscale on Y axis")) + self.autoscale.setText(_translate("RT_Plotter", "auto scale")) + self.constant_input.setToolTip(_translate("RT_Plotter", "enter a number and press enter to draw a line")) + self.offset_spin.setToolTip(_translate("RT_Plotter", "offset")) + self.action_new_plot.setText(_translate("RT_Plotter", "New plot")) + self.action_new_plot.setShortcut(_translate("RT_Plotter", "Ctrl+N")) + self.action_reset.setText(_translate("RT_Plotter", "Reset")) + self.action_reset.setToolTip(_translate("RT_Plotter", "reset data for all plots")) + self.action_reset.setShortcut(_translate("RT_Plotter", "Ctrl+L")) + self.action_suspend.setText(_translate("RT_Plotter", "Suspend")) + self.action_suspend.setToolTip(_translate("RT_Plotter", "freeze plotter view")) + self.action_suspend.setShortcut(_translate("RT_Plotter", "Ctrl+S")) + self.action_restart.setText(_translate("RT_Plotter", "Restart")) + self.action_restart.setToolTip(_translate("RT_Plotter", "restart suspended plotter")) + self.action_restart.setShortcut(_translate("RT_Plotter", "Ctrl+X")) + self.action_close.setText(_translate("RT_Plotter", "Close")) + self.action_close.setToolTip(_translate("RT_Plotter", "close plotter window")) + self.action_close.setShortcut(_translate("RT_Plotter", "Ctrl+W")) + self.action_quit.setText(_translate("RT_Plotter", "Quit")) + self.action_quit.setToolTip(_translate("RT_Plotter", "quit realtime plotter (close all windows)")) + self.action_quit.setShortcut(_translate("RT_Plotter", "Ctrl+Q")) + self.action_dark_background.setText(_translate("RT_Plotter", "Dark background")) + self.action_dark_background.setToolTip(_translate("RT_Plotter", "change background from white to black")) + self.action_dark_background.setShortcut(_translate("RT_Plotter", "Ctrl+B"))