[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 <fabien.bonneval@gmail.com>
This commit is contained in:
Gautier Hattenberger
2023-02-24 00:20:05 +01:00
committed by GitHub
parent 3badcfa367
commit 6169709da6
10 changed files with 799 additions and 881 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
<program command="sw/ground_segment/python/real_time_plot/realtimeplotapp.py" name="Real-time Plotter (Python)" icon="plotter_python.svg"/>
<program command="sw/logalizer/rt_plotter.py" name="Real-time Plotter (Python)" icon="plotter_python.svg"/>
@@ -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()
@@ -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
@@ -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)
@@ -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()
@@ -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)
+5
View File
@@ -0,0 +1,5 @@
PyQt5
pyqtgraph
numpy
ivy-python
+334
View File
@@ -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()
+302
View File
@@ -0,0 +1,302 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RT_Plotter</class>
<widget class="QWidget" name="RT_Plotter">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>910</width>
<height>404</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops">
<bool>false</bool>
</property>
<property name="windowTitle">
<string>Real-time plotter</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<widget class="QLabel" name="label_3">
<property name="text">
<string>update</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="dt_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>0.50 s</string>
</property>
<property name="margin">
<number>5</number>
</property>
</widget>
</item>
<item row="0" column="5">
<widget class="QLabel" name="size_label">
<property name="text">
<string>30 s</string>
</property>
<property name="margin">
<number>3</number>
</property>
</widget>
</item>
<item row="0" column="13">
<widget class="QToolButton" name="menu_button">
<property name="contextMenuPolicy">
<enum>Qt::ActionsContextMenu</enum>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset theme="format-justify-fill"/>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QLabel" name="label_5">
<property name="text">
<string>size</string>
</property>
</widget>
</item>
<item row="0" column="10">
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>scale next by</string>
</property>
<property name="margin">
<number>0</number>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QSlider" name="dt_slider">
<property name="toolTip">
<string>update time (s)</string>
</property>
<property name="minimum">
<number>5</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="sliderPosition">
<number>50</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="0" column="7">
<widget class="QLabel" name="label">
<property name="text">
<string>constant</string>
</property>
</widget>
</item>
<item row="0" column="6">
<widget class="QSlider" name="size_slider">
<property name="toolTip">
<string>display time interval</string>
</property>
<property name="minimum">
<number>10</number>
</property>
<property name="maximum">
<number>240</number>
</property>
<property name="value">
<number>30</number>
</property>
<property name="sliderPosition">
<number>30</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="0" column="11">
<widget class="QDoubleSpinBox" name="scale_spin">
<property name="toolTip">
<string>scaling factor</string>
</property>
<property name="minimum">
<double>-9999.000000000000000</double>
</property>
<property name="maximum">
<double>9999.989999999999782</double>
</property>
<property name="value">
<double>1.000000000000000</double>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QPushButton" name="autoscale">
<property name="toolTip">
<string>restart autoscale on Y axis</string>
</property>
<property name="text">
<string>auto scale</string>
</property>
</widget>
</item>
<item row="0" column="8">
<widget class="QLineEdit" name="constant_input">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>45</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>enter a number and press enter to draw a line</string>
</property>
</widget>
</item>
<item row="0" column="12">
<widget class="QDoubleSpinBox" name="offset_spin">
<property name="toolTip">
<string>offset</string>
</property>
<property name="minimum">
<double>-9999.000000000000000</double>
</property>
<property name="maximum">
<double>9999.989999999999782</double>
</property>
</widget>
</item>
<item row="0" column="9">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
<action name="action_new_plot">
<property name="text">
<string>New plot</string>
</property>
<property name="shortcut">
<string>Ctrl+N</string>
</property>
</action>
<action name="action_reset">
<property name="text">
<string>Reset</string>
</property>
<property name="toolTip">
<string>reset data for all plots</string>
</property>
<property name="shortcut">
<string>Ctrl+L</string>
</property>
</action>
<action name="action_suspend">
<property name="text">
<string>Suspend</string>
</property>
<property name="toolTip">
<string>freeze plotter view</string>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</action>
<action name="action_restart">
<property name="text">
<string>Restart</string>
</property>
<property name="toolTip">
<string>restart suspended plotter</string>
</property>
<property name="shortcut">
<string>Ctrl+X</string>
</property>
</action>
<action name="action_close">
<property name="text">
<string>Close</string>
</property>
<property name="toolTip">
<string>close plotter window</string>
</property>
<property name="shortcut">
<string>Ctrl+W</string>
</property>
</action>
<action name="action_quit">
<property name="text">
<string>Quit</string>
</property>
<property name="toolTip">
<string>quit realtime plotter (close all windows)</string>
</property>
<property name="shortcut">
<string>Ctrl+Q</string>
</property>
</action>
<action name="action_dark_background">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Dark background</string>
</property>
<property name="toolTip">
<string>change background from white to black</string>
</property>
<property name="shortcut">
<string>Ctrl+B</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>
+157
View File
@@ -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"))