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)
class MessageInterface: """ MessageInterface This is an abstraction layer used to simplify the use of paparazzi message interface, especially for time measurement and casting of message payload data (which sometimes stays in ascii) """ def prettify_message(msg): """ Sometimes IvyMessageInterface does not cast data to their binary types. This function cast all fields to their binary types. (supposed to be done by PprzMessage.payload_to_binay but does not seem to be working) It also measure reception time. """ timestamp = time.time() fieldValues = [] for fieldValue, fieldType in zip(msg.fieldvalues, msg.fieldtypes): if "int" in fieldType: castType = int elif "float" in fieldType: castType = float elif "string" in fieldType: castType = str elif "char" in fieldType: castType = int else: # Could not indentify type, leave field as is fieldValues.append(fieldValue) # Checking if is a list if '[' in fieldType: fieldValues.append([castType(value) for value in fieldValue]) else: fieldValues.append(castType(fieldValue)) msg.set_values(fieldValues) msg.timestamp = timestamp return msg def parse_pprz_msg(msg): """ Alias to IvyMessageInterface.parse_pprz_msg, but with prettify_message called at the end to ensure all data are in binary format. """ class Catcher: """ This is a type specifically to catch result from IvyMessageInterface.parse_pprz_msg which only outputs result via a callback. """ def set_message(self, aircraftId, message): self.message = message self.aircraftId = str(aircraftId) catcher = Catcher() IvyMessagesInterface.parse_pprz_msg(catcher.set_message, msg) return [MessageInterface.prettify_message(catcher.message), catcher.aircraftId] def __init__(self, ivyBusAddress=None): if ivyBusAddress is None: ivyBusAddress = os.getenv('IVY_BUS') if ivyBusAddress is None: ivyBusAddress == "" self.messageInterface = IvyMessagesInterface(ivy_bus=ivyBusAddress) def bind(self, callback, ivyRegex): return self.messageInterface.subscribe( lambda sender, msg: callback(MessageInterface.prettify_message(msg)), ivyRegex) def bind_raw(self, callback, ivyRegex): return self.messageInterface.bind_raw(callback, ivyRegex) def unbind(self, bindId): self.messageInterface.unbind(bindId) def send(self, msg): return self.messageInterface.send(msg)
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)