mirror of
https://github.com/paparazzi/paparazzi.git
synced 2026-05-09 22:49:53 +08:00
[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:
committed by
GitHub
parent
3badcfa367
commit
6169709da6
@@ -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)
|
||||
@@ -0,0 +1,5 @@
|
||||
PyQt5
|
||||
pyqtgraph
|
||||
numpy
|
||||
ivy-python
|
||||
|
||||
Executable
+334
@@ -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()
|
||||
@@ -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>
|
||||
@@ -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"))
|
||||
Reference in New Issue
Block a user