# HG changeset patch # User Edouard Tisserant # Date 1623860847 -7200 # Node ID 6bd91873204743d9015d1e9997764f542504c888 # Parent 76da573569a69fcaffe6580894c10a385e902a28# Parent 5ce56021f16658177b51ee749dddbe9612b35218 merge diff -r 76da573569a6 -r 6bd918732047 BeremizIDE.py --- a/BeremizIDE.py Wed Jun 16 18:27:05 2021 +0200 +++ b/BeremizIDE.py Wed Jun 16 18:27:27 2021 +0200 @@ -223,7 +223,7 @@ return False def progress(self, text): - l = self.output.GetLineCount()-2 + l = max(self.output.GetLineCount()-2, 0) self.output.AnnotationSetText(l, text) self.output.AnnotationSetVisible(wx.stc.STC_ANNOTATION_BOXED) self.output.AnnotationSetStyle(l, self.black_white) @@ -496,6 +496,7 @@ self.local_runtime_tmpdir = tempfile.mkdtemp() # choose an arbitrary random port for runtime self.runtime_port = int(random.random() * 1000) + 61131 + self.Log.write(_("Starting local runtime...\n")) # launch local runtime self.local_runtime = ProcessLogger( self.Log, diff -r 76da573569a6 -r 6bd918732047 ProjectController.py --- a/ProjectController.py Wed Jun 16 18:27:05 2021 +0200 +++ b/ProjectController.py Wed Jun 16 18:27:27 2021 +0200 @@ -40,6 +40,7 @@ from datetime import datetime from weakref import WeakKeyDictionary from functools import reduce +from itertools import izip from distutils.dir_util import copy_tree from six.moves import xrange @@ -1512,8 +1513,8 @@ for debug_tick, debug_buff in Traces: debug_vars = UnpackDebugBuffer( debug_buff, self.TracedIECTypes) - if debug_vars is not None and len(debug_vars) == len(self.TracedIECPath): - for IECPath, values_buffer, value in zip( + if debug_vars is not None: + for IECPath, values_buffer, value in izip( self.TracedIECPath, self.DebugValuesBuffers, debug_vars): @@ -1606,8 +1607,8 @@ WeakKeyDictionary(), # Callables [], # Data storage [(tick, data),...] "Registered", # Variable status - None, - buffer_list] # Forced value + None, # Forced value + buffer_list] self.IECdebug_datas[IECPath] = IECdebug_data else: IECdebug_data[4] |= buffer_list @@ -1681,27 +1682,28 @@ return self._connector.RemoteExec(script, **kwargs) def DispatchDebugValuesProc(self, event): + event.Skip() + start_time = time.time() self.debug_status, debug_ticks, buffers = self.SnapshotAndResetDebugValuesBuffers() - start_time = time.time() - if len(self.TracedIECPath) == len(buffers): - for IECPath, values in zip(self.TracedIECPath, buffers): - if len(values) > 0: - self.CallWeakcallables( - IECPath, "NewValues", debug_ticks, values) - if len(debug_ticks) > 0: - self.CallWeakcallables( - "__tick__", "NewDataAvailable", debug_ticks) if self.debug_status == PlcStatus.Broken: self.logger.write_warning( _("Debug: token rejected - other debug took over - reconnect to recover\n")) - else: - delay = time.time() - start_time - next_refresh = max(REFRESH_PERIOD - delay, 0.2 * delay) - if self.DispatchDebugValuesTimer is not None: - self.DispatchDebugValuesTimer.Start( - int(next_refresh * 1000), oneShot=True) - event.Skip() + return + + for IECPath, values in zip(self.TracedIECPath, buffers): + if len(values) > 0: + self.CallWeakcallables( + IECPath, "NewValues", debug_ticks, values) + if len(debug_ticks) > 0: + self.CallWeakcallables( + "__tick__", "NewDataAvailable", debug_ticks) + + delay = time.time() - start_time + next_refresh = max(REFRESH_PERIOD - delay, 0.2 * delay) + if self.DispatchDebugValuesTimer is not None: + res = self.DispatchDebugValuesTimer.Start( + int(next_refresh * 1000), oneShot=True) def KillDebugThread(self): if self.DispatchDebugValuesTimer is not None: diff -r 76da573569a6 -r 6bd918732047 controls/DebugVariablePanel/DebugVariableItem.py --- a/controls/DebugVariablePanel/DebugVariableItem.py Wed Jun 16 18:27:05 2021 +0200 +++ b/controls/DebugVariablePanel/DebugVariableItem.py Wed Jun 16 18:27:27 2021 +0200 @@ -26,8 +26,9 @@ from __future__ import absolute_import from datetime import timedelta import binascii -import numpy +import numpy as np from graphics.DebugDataConsumer import DebugDataConsumer, TYPE_TRANSLATOR +from controls.DebugVariablePanel.RingBuffer import RingBuffer # ------------------------------------------------------------------------------- # Constant for calculate CRC for string variables @@ -142,8 +143,8 @@ @return: Data as numpy.array([(tick, value, forced),...]) """ # Return immediately if data empty or none - if self.Data is None or len(self.Data) == 0: - return self.Data + if self.Data is None or self.Data.count == 0: + return None # Find nearest data outside given range indexes start_idx = (self.GetNearestData(start_tick, -1) @@ -154,7 +155,7 @@ else len(self.Data)) # Return data between indexes - return self.Data[start_idx:end_idx] + return self.Data.view[start_idx:end_idx] def GetRawValue(self, index): """ @@ -195,8 +196,8 @@ if len(values) > 0: # Return value range for data in given tick range return (data, - data[numpy.argmin(values), 1], - data[numpy.argmax(values), 1]) + data[np.argmin(values), 1], + data[np.argmax(values), 1]) # Return default values return data, None, None @@ -207,7 +208,7 @@ """ if self.StoreData and self.IsNumVariable(): # Init table storing data - self.Data = numpy.array([]).reshape(0, 3) + self.Data = RingBuffer(3) # Init table storing raw data if variable is strin self.RawData = ([] @@ -294,7 +295,7 @@ [float(tick), num_value, extra_value]) # Add New data to stored data table - self.Data = numpy.append(self.Data, data_values, axis=0) + self.Data.append(data_values) # Signal to debug variable panel to refresh self.Parent.HasNewData = True @@ -337,7 +338,7 @@ if tick is not None and self.Data is not None: # Return current value and forced flag if data empty - if len(self.Data) == 0: + if self.Data.count == 0: return self.Value, self.IsForced() # Get index of nearest data from tick given @@ -345,9 +346,9 @@ # Get value and forced flag at given index value, forced = \ - self.RawData[int(self.Data[idx, 2])] \ + self.RawData[int(self.Data.view[idx, 2])] \ if self.VariableType in ["STRING", "WSTRING"] \ - else self.Data[idx, 1:3] + else self.Data.view[idx, 1:3] if self.VariableType in ["TIME", "TOD", "DT", "DATE"]: value = timedelta(seconds=value) @@ -380,10 +381,10 @@ return None # Extract data ticks - ticks = self.Data[:, 0] + ticks = self.Data.view[:, 0] # Get nearest data from tick - idx = numpy.argmin(abs(ticks - tick)) + idx = min(np.searchsorted(ticks, tick), self.Data.count - 1) # Adjust data index according to constraint if adjust < 0 and ticks[idx] > tick and idx > 0 or \ diff -r 76da573569a6 -r 6bd918732047 controls/DebugVariablePanel/DebugVariablePanel.py --- a/controls/DebugVariablePanel/DebugVariablePanel.py Wed Jun 16 18:27:05 2021 +0200 +++ b/controls/DebugVariablePanel/DebugVariablePanel.py Wed Jun 16 18:27:27 2021 +0200 @@ -26,7 +26,7 @@ from __future__ import absolute_import from __future__ import division from functools import reduce -import numpy +import numpy as np import wx import wx.lib.buttons @@ -43,6 +43,7 @@ from controls.DebugVariablePanel.DebugVariableItem import DebugVariableItem from controls.DebugVariablePanel.DebugVariableTextViewer import DebugVariableTextViewer from controls.DebugVariablePanel.DebugVariableGraphicViewer import * +from controls.DebugVariablePanel.RingBuffer import RingBuffer MILLISECOND = 1000000 # Number of nanosecond in a millisecond @@ -205,7 +206,7 @@ main_sizer = wx.BoxSizer(wx.VERTICAL) - self.Ticks = numpy.array([]) # List of tick received + self.Ticks = RingBuffer() # List of tick received self.StartTick = 0 # Tick starting range of data displayed self.Fixed = False # Flag that range of data is fixed self.CursorTick = None # Tick of cursor for displaying values @@ -344,11 +345,11 @@ tick = ticks[-1] # Save tick as start tick for range if data is still empty - if len(self.Ticks) == 0: + if self.Ticks.count == 0: self.StartTick = ticks[0] # Add tick to list of ticks received - self.Ticks = numpy.append(self.Ticks, ticks) + self.Ticks.append(ticks) # Update start tick for range if range follow ticks received if not self.Fixed or tick < self.StartTick + self.CurrentRange: @@ -356,9 +357,13 @@ # Force refresh if graph is fixed because range of data received # is too small to fill data range selected - if self.Fixed and \ - self.Ticks[-1] - self.Ticks[0] < self.CurrentRange: - self.Force = True + if self.Fixed : + if self.Ticks.view[-1] - self.Ticks.view[0] < self.CurrentRange: + self.Force = True + if self.Ticks.view[0] > self.StartTick: + self.StartTick = self.Ticks.view[0] + self.Force = True + self.HasNewData = False self.RefreshView() @@ -385,17 +390,16 @@ def MoveCursorTick(self, move): if self.CursorTick is not None: - cursor_tick = max(self.Ticks[0], - min(self.CursorTick + move, self.Ticks[-1])) - cursor_tick_idx = numpy.argmin(numpy.abs(self.Ticks - cursor_tick)) - if self.Ticks[cursor_tick_idx] == self.CursorTick: + cursor_tick = max(self.Ticks.view[0], + min(self.CursorTick + move, self.Ticks.view[-1])) + cursor_tick_idx = min(np.searchsorted(self.Ticks.view, cursor_tick), self.Ticks.count - 1) + if self.Ticks.view[cursor_tick_idx] == self.CursorTick: cursor_tick_idx = max(0, min(cursor_tick_idx + abs(move) // move, - len(self.Ticks) - 1)) - self.CursorTick = self.Ticks[cursor_tick_idx] + self.Ticks.count - 1)) + self.CursorTick = self.Ticks.view[cursor_tick_idx] self.StartTick = max( - self.Ticks[numpy.argmin( - numpy.abs(self.Ticks - self.CursorTick + self.CurrentRange))], + self.Ticks.view[min(np.searchsorted(self.Ticks.view, self.CursorTick - self.CurrentRange), self.Ticks.count - 1)], min(self.StartTick, self.CursorTick)) self.RefreshCanvasPosition() self.UpdateCursorTick() @@ -547,8 +551,8 @@ if self.CursorTick is not None: tick = self.CursorTick - elif len(self.Ticks) > 0: - tick = self.Ticks[-1] + elif self.Ticks.count > 0: + tick = self.Ticks.view[-1] else: tick = None if tick is not None: @@ -604,16 +608,16 @@ self.RefreshGraphicsSizer() def SetCanvasPosition(self, tick): - tick = max(self.Ticks[0], min(tick, self.Ticks[-1] - self.CurrentRange)) - self.StartTick = self.Ticks[numpy.argmin(numpy.abs(self.Ticks - tick))] + tick = max(self.Ticks.view[0], min(tick, self.Ticks.view[-1] - self.CurrentRange)) + self.StartTick = self.Ticks.view[min(np.searchsorted(self.Ticks.view, tick), self.Ticks.count - 1)] self.Fixed = True self.RefreshCanvasPosition() self.ForceRefresh() def RefreshCanvasPosition(self): - if len(self.Ticks) > 0: - pos = int(self.StartTick - self.Ticks[0]) - range = int(self.Ticks[-1] - self.Ticks[0]) + if len(self.Ticks.view) > 0: + pos = int(self.StartTick - self.Ticks.view[0]) + range = int(self.Ticks.view[-1] - self.Ticks.view[0]) else: pos = 0 range = 0 @@ -626,23 +630,23 @@ if new_range_idx != current_range_idx: self.CanvasRange.SetSelection(new_range_idx) self.CurrentRange = self.RANGE_VALUES[new_range_idx][1] / self.Ticktime - if len(self.Ticks) > 0: + if self.Ticks.count > 0: if tick is None: tick = self.StartTick + self.CurrentRange / 2. new_start_tick = min(tick - (tick - self.StartTick) * self.CurrentRange / current_range, - self.Ticks[-1] - self.CurrentRange) - self.StartTick = self.Ticks[numpy.argmin(numpy.abs(self.Ticks - new_start_tick))] - self.Fixed = new_start_tick < self.Ticks[-1] - self.CurrentRange + self.Ticks.view[-1] - self.CurrentRange) + self.StartTick = self.Ticks.view[min(np.searchsorted(self.Ticks.view, new_start_tick), self.Ticks.count - 1)] + self.Fixed = new_start_tick < self.Ticks.view[-1] - self.CurrentRange self.ForceRefresh() def RefreshRange(self): - if len(self.Ticks) > 0: - if self.Fixed and self.Ticks[-1] - self.Ticks[0] < self.CurrentRange: + if self.Ticks.count > 0: + if self.Fixed and self.Ticks.view[-1] - self.Ticks.view[0] < self.CurrentRange: self.Fixed = False if self.Fixed: - self.StartTick = min(self.StartTick, self.Ticks[-1] - self.CurrentRange) - else: - self.StartTick = max(self.Ticks[0], self.Ticks[-1] - self.CurrentRange) + self.StartTick = min(self.StartTick, self.Ticks.view[-1] - self.CurrentRange) + else: + self.StartTick = max(self.Ticks.view[0], self.Ticks.view[-1] - self.CurrentRange) self.ForceRefresh() def OnRangeChanged(self, event): @@ -654,8 +658,8 @@ event.Skip() def OnCurrentButton(self, event): - if len(self.Ticks) > 0: - self.StartTick = max(self.Ticks[0], self.Ticks[-1] - self.CurrentRange) + if self.Ticks.count > 0: + self.StartTick = max(self.Ticks.view[0], self.Ticks.view[-1] - self.CurrentRange) self.ResetCursorTick() event.Skip() @@ -695,8 +699,8 @@ event.Skip() def OnPositionChanging(self, event): - if len(self.Ticks) > 0: - self.StartTick = self.Ticks[0] + event.GetPosition() + if self.Ticks.count > 0: + self.StartTick = self.Ticks.view[0] + event.GetPosition() self.Fixed = True self.ForceRefresh() event.Skip() @@ -908,7 +912,7 @@ self.ForceRefresh() def ResetGraphicsValues(self): - self.Ticks = numpy.array([]) + self.Ticks = RingBuffer() self.StartTick = 0 for panel in self.GraphicPanels: panel.ResetItemsData() diff -r 76da573569a6 -r 6bd918732047 controls/DebugVariablePanel/DebugVariableTextViewer.py --- a/controls/DebugVariablePanel/DebugVariableTextViewer.py Wed Jun 16 18:27:05 2021 +0200 +++ b/controls/DebugVariablePanel/DebugVariableTextViewer.py Wed Jun 16 18:27:27 2021 +0200 @@ -270,7 +270,7 @@ """ # Execute callback on button under mouse pointer if it exists x, y = event.GetPosition() - wx.CallAfter(self.HandleButton, x, y) + self.HandleButton(x, y) event.Skip() def OnLeftDClick(self, event): diff -r 76da573569a6 -r 6bd918732047 controls/DebugVariablePanel/RingBuffer.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/controls/DebugVariablePanel/RingBuffer.py Wed Jun 16 18:27:27 2021 +0200 @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of Beremiz +# Copyright (C) 2021: Edouard TISSERANT +# +# See COPYING file for copyrights details. + +# Based on Eelco Hoogendoorn stackoverflow answer about RingBuffer with numpy + +import numpy as np + + +class RingBuffer(object): + def __init__(self, width=None, size=131072, padding=None): + self.size = size + self.padding = size if padding is None else padding + shape = (self.size+self.padding,) + if width : + shape += (width,) + self.buffer = np.zeros(shape) + self.cursor = 0 + + def append(self, data): + """this is an O(n) operation""" + data = data[-self.size:] + n = len(data) + if self.size + self.padding - self.cursor < n: + self.compact() + self.buffer[self.cursor:][:n] = data + self.cursor += n + + @property + def count(self): + return min(self.size, self.cursor) + + @property + def view(self): + """this is always an O(1) operation""" + return self.buffer[max(0, self.cursor - self.size):][:self.count] + + def compact(self): + """ + note: only when this function is called, is an O(size) performance hit incurred, + and this cost is amortized over the whole padding space + """ + print 'compacting' + self.buffer[:self.count] = self.view + self.cursor -= self.size + diff -r 76da573569a6 -r 6bd918732047 runtime/typemapping.py --- a/runtime/typemapping.py Wed Jun 16 18:27:05 2021 +0200 +++ b/runtime/typemapping.py Wed Jun 16 18:27:27 2021 +0200 @@ -92,7 +92,7 @@ buffoffset += sizeof(c_type) if iectype != "STRING" else len(value)+1 res.append(value) else: - break + return None if buffoffset and buffoffset == buffsize: return res return None