Added new debug process separating non-wx thread extracting debug values from connector and 10 Hz wx timer refreshing Beremiz debug Viewers and communicating throw double-buffering, to avoid segmentation faults and optimize CPU usage
--- a/ProjectController.py Sat Oct 12 10:10:30 2013 +0900
+++ b/ProjectController.py Mon Oct 14 10:31:32 2013 +0200
@@ -22,7 +22,7 @@
from editors.FileManagementPanel import FileManagementPanel
from editors.ProjectNodeEditor import ProjectNodeEditor
from editors.IECCodeViewer import IECCodeViewer
-from editors.DebugViewer import DebugViewer
+from editors.DebugViewer import DebugViewer, REFRESH_PERIOD
from dialogs import DiscoveryDialog
from PLCControler import PLCControler
from plcopen.structures import IEC_KEYWORDS
@@ -110,6 +110,9 @@
self.MandatoryParams = None
self._builder = None
self._connector = None
+ self.DispatchDebugValuesTimer = None
+ self.DebugValuesBuffers = []
+ self.DebugTicks = []
self.SetAppFrame(frame, logger)
self.iec2c_path = os.path.join(base_folder, "matiec", "iec2c"+(".exe" if wx.Platform == '__WXMSW__' else ""))
@@ -157,15 +160,23 @@
self.AppFrame = frame
self.logger = logger
self.StatusTimer = None
+ if self.DispatchDebugValuesTimer is not None:
+ self.DispatchDebugValuesTimer.Stop()
+ self.DispatchDebugValuesTimer = None
if frame is not None:
frame.LogViewer.SetLogSource(self._connector)
# Timer to pull PLC status
- ID_STATUSTIMER = wx.NewId()
- self.StatusTimer = wx.Timer(self.AppFrame, ID_STATUSTIMER)
- self.AppFrame.Bind(wx.EVT_TIMER, self.PullPLCStatusProc, self.StatusTimer)
-
+ self.StatusTimer = wx.Timer(self.AppFrame, -1)
+ self.AppFrame.Bind(wx.EVT_TIMER,
+ self.PullPLCStatusProc, self.StatusTimer)
+
+ # Timer to dispatch debug values to consumers
+ self.DispatchDebugValuesTimer = wx.Timer(self.AppFrame, -1)
+ self.AppFrame.Bind(wx.EVT_TIMER,
+ self.DispatchDebugValuesProc, self.DispatchDebugValuesTimer)
+
self.RefreshConfNodesBlockLists()
def ResetAppFrame(self, logger):
@@ -1177,6 +1188,12 @@
def PullPLCStatusProc(self, event):
self.UpdateMethodsFromPLCStatus()
+ def SnapshotAndResetDebugValuesBuffers(self):
+ buffers, self.DebugValuesBuffers = (self.DebugValuesBuffers,
+ [list() for iec_path in self.TracedIECPath])
+ ticks, self.DebugTicks = self.DebugTicks, []
+ return ticks, buffers
+
def RegisterDebugVarToConnector(self):
self.DebugTimer=None
Idxs = []
@@ -1210,11 +1227,12 @@
else:
self.TracedIECPath = []
self._connector.SetTraceVariablesList([])
+ self.SnapshotAndResetDebugValuesBuffers()
self.IECdebug_lock.release()
def IsPLCStarted(self):
return self.previous_plcstate == "Started"
-
+
def ReArmDebugRegisterTimer(self):
if self.DebugTimer is not None:
self.DebugTimer.cancel()
@@ -1233,7 +1251,7 @@
Idx, IEC_Type = self._IECPathToIdx.get(IECPath,(None,None))
return IEC_Type
- def SubscribeDebugIECVariable(self, IECPath, callableobj, *args, **kwargs):
+ def SubscribeDebugIECVariable(self, IECPath, callableobj, buffer_list=False, *args, **kwargs):
"""
Dispatching use a dictionnary linking IEC variable paths
to a WeakKeyDictionary linking
@@ -1250,7 +1268,8 @@
WeakKeyDictionary(), # Callables
[], # Data storage [(tick, data),...]
"Registered", # Variable status
- None] # Forced value
+ None,
+ buffer_list] # Forced value
self.IECdebug_datas[IECPath] = IECdebug_data
IECdebug_data[0][callableobj]=(args, kwargs)
@@ -1347,14 +1366,14 @@
#print [dict.keys() for IECPath, (dict, log, status, fvalue) in self.IECdebug_datas.items()]
if plc_status == "Started":
self.IECdebug_lock.acquire()
- if len(debug_vars) == len(self.TracedIECPath):
+ if len(debug_vars) == len(self.DebugValuesBuffers):
if debug_getvar_retry > DEBUG_RETRIES_WARN:
self.logger.write(_("... debugger recovered\n"))
debug_getvar_retry = 0
- for IECPath,value in zip(self.TracedIECPath, debug_vars):
+ for values_buffer, value in zip(self.DebugValuesBuffers, debug_vars):
if value is not None:
- self.CallWeakcallables(IECPath, "NewValue", debug_tick, value)
- self.CallWeakcallables("__tick__", "NewDataAvailable", debug_tick)
+ values_buffer.append(value)
+ self.DebugTicks.append(debug_tick)
self.IECdebug_lock.release()
if debug_getvar_retry == DEBUG_RETRIES_WARN:
self.logger.write(_("Waiting debugger to recover...\n"))
@@ -1368,6 +1387,20 @@
self.debug_break = True
self.logger.write(_("Debugger disabled\n"))
self.DebugThread = None
+ if self.DispatchDebugValuesTimer is not None:
+ self.DispatchDebugValuesTimer.Stop()
+
+ def DispatchDebugValuesProc(self, event):
+ self.IECdebug_lock.acquire()
+ debug_ticks, buffers = self.SnapshotAndResetDebugValuesBuffers()
+ self.IECdebug_lock.release()
+ 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)
+ event.Skip()
def KillDebugThread(self):
tmp_debugthread = self.DebugThread
@@ -1380,12 +1413,16 @@
else:
self.logger.write(_("Debugger stopped.\n"))
self.DebugThread = None
+ if self.DispatchDebugValuesTimer is not None:
+ self.DispatchDebugValuesTimer.Stop()
def _connect_debug(self):
self.previous_plcstate = None
if self.AppFrame:
self.AppFrame.ResetGraphicViewers()
self.RegisterDebugVarToConnector()
+ if self.DispatchDebugValuesTimer is not None:
+ self.DispatchDebugValuesTimer.Start(int(REFRESH_PERIOD * 1000))
if self.DebugThread is None:
self.DebugThread = Thread(target=self.DebugThreadProc)
self.DebugThread.start()
--- a/controls/DebugVariablePanel/DebugVariableGraphicPanel.py Sat Oct 12 10:10:30 2013 +0900
+++ b/controls/DebugVariablePanel/DebugVariableGraphicPanel.py Mon Oct 14 10:31:32 2013 +0200
@@ -330,7 +330,7 @@
DebugViewer.RefreshNewData(self, *args, **kwargs)
- def NewDataAvailable(self, tick, *args, **kwargs):
+ def NewDataAvailable(self, ticks, *args, **kwargs):
"""
Called by DataProducer for each tick captured or by panel to refresh
graphs
@@ -338,15 +338,15 @@
All other parameters are passed to refresh function
"""
# If tick given
- if tick is not None:
- self.HasNewData = True
+ if ticks is not None:
+ tick = ticks[-1]
# Save tick as start tick for range if data is still empty
if len(self.Ticks) == 0:
- self.StartTick = tick
+ self.StartTick = ticks[0]
# Add tick to list of ticks received
- self.Ticks = numpy.append(self.Ticks, [tick])
+ self.Ticks = numpy.append(self.Ticks, ticks)
# Update start tick for range if range follow ticks received
if not self.Fixed or tick < self.StartTick + self.CurrentRange:
@@ -357,8 +357,12 @@
if self.Fixed and \
self.Ticks[-1] - self.Ticks[0] < self.CurrentRange:
self.Force = True
-
- DebugViewer.NewDataAvailable(self, tick, *args, **kwargs)
+
+ self.HasNewData = False
+ self.RefreshView()
+
+ else:
+ DebugViewer.NewDataAvailable(self, ticks, *args, **kwargs)
def ForceRefresh(self):
"""
--- a/controls/DebugVariablePanel/DebugVariableItem.py Sat Oct 12 10:10:30 2013 +0900
+++ b/controls/DebugVariablePanel/DebugVariableItem.py Mon Oct 14 10:31:32 2013 +0200
@@ -232,52 +232,63 @@
return (self.Parent.IsNumType(self.VariableType) or
self.VariableType in ["STRING", "WSTRING"])
- def NewValue(self, tick, value, forced=False):
+ def NewValues(self, ticks, values, forced=False):
"""
Function called by debug thread when a new debug value is available
@param tick: PLC tick when value was captured
@param value: Value captured
@param forced: Forced flag, True if value is forced (default: False)
"""
- DebugDataConsumer.NewValue(self, tick, value, forced, raw=None)
+ DebugDataConsumer.NewValues(self, ticks, values, forced, raw=None)
if self.Data is not None:
- # String data value is CRC
- num_value = (binascii.crc32(value) & STRING_CRC_MASK
- if self.VariableType in ["STRING", "WSTRING"]
- else float(value))
-
- # Update variable range values
- self.MinValue = (min(self.MinValue, num_value)
- if self.MinValue is not None
- else num_value)
- self.MaxValue = (max(self.MaxValue, num_value)
- if self.MaxValue is not None
- else num_value)
-
+
+ if self.VariableType in ["STRING", "WSTRING"]:
+ last_raw_data = (self.RawData[-1]
+ if len(self.RawData) > 0 else None)
+ last_raw_data_idx = len(self.RawData) - 1
+
# Translate forced flag to float for storing in Data table
forced_value = float(forced)
- # In the case of string variables, we store raw string value and
- # forced flag in raw data table. Only changes in this two values
- # are stored. Index to the corresponding raw value is stored in
- # data third column
- if self.VariableType in ["STRING", "WSTRING"]:
- raw_data = (value, forced_value)
- if len(self.RawData) == 0 or self.RawData[-1] != raw_data:
- extra_value = len(self.RawData)
- self.RawData.append(raw_data)
+ data_values = []
+ for tick, value in zip(ticks, values):
+
+ # String data value is CRC
+ num_value = (binascii.crc32(value) & STRING_CRC_MASK
+ if self.VariableType in ["STRING", "WSTRING"]
+ else float(value))
+
+ # Update variable range values
+ self.MinValue = (min(self.MinValue, num_value)
+ if self.MinValue is not None
+ else num_value)
+ self.MaxValue = (max(self.MaxValue, num_value)
+ if self.MaxValue is not None
+ else num_value)
+
+ # In the case of string variables, we store raw string value and
+ # forced flag in raw data table. Only changes in this two values
+ # are stored. Index to the corresponding raw value is stored in
+ # data third column
+ if self.VariableType in ["STRING", "WSTRING"]:
+ raw_data = (value, forced_value)
+ if len(self.RawData) == 0 or last_raw_data != raw_data:
+ last_raw_data_idx += 1
+ last_raw_data = raw_data
+ self.RawData.append(raw_data)
+ extra_value = last_raw_data_idx
+
+ # In other case, data third column is forced flag
else:
- extra_value = len(self.RawData) - 1
-
- # In other case, data third column is forced flag
- else:
- extra_value = forced_value
+ extra_value = forced_value
+
+ data_values.append(
+ [float(tick), num_value, extra_value])
# Add New data to stored data table
- self.Data = numpy.append(self.Data,
- [[float(tick), num_value, extra_value]], axis=0)
-
+ self.Data = numpy.append(self.Data, data_values, axis=0)
+
# Signal to debug variable panel to refresh
self.Parent.HasNewData = True
--- a/editors/DebugViewer.py Sat Oct 12 10:10:30 2013 +0900
+++ b/editors/DebugViewer.py Mon Oct 14 10:31:32 2013 +0200
@@ -214,7 +214,7 @@
# Search for variable informations in project data
infos = self.DataProducer.GetInstanceInfos(iec_path)
if infos is not None:
- return infos["type"]
+ return infos.type
return None
@@ -246,7 +246,7 @@
if self.DataProducer is not None:
self.DataProducer.ReleaseDebugIECVariable(iec_path)
- def NewDataAvailable(self, tick, *args, **kwargs):
+ def NewDataAvailable(self, ticks, *args, **kwargs):
"""
Called by DataProducer for each tick captured
@param tick: PLC tick captured
--- a/editors/Viewer.py Sat Oct 12 10:10:30 2013 +0900
+++ b/editors/Viewer.py Mon Oct 14 10:31:32 2013 +0200
@@ -1053,7 +1053,7 @@
self.ElementRefreshList.append(element)
self.ElementRefreshList_lock.release()
- def RefreshNewData(self):
+ def NewDataAvailable(self, ticks, *args, **kwargs):
refresh_rect = None
self.ElementRefreshList_lock.acquire()
for element in self.ElementRefreshList:
@@ -1066,8 +1066,6 @@
if refresh_rect is not None:
self.RefreshRect(self.GetScrolledRect(refresh_rect), False)
- else:
- DebugViewer.RefreshNewData(self)
def SubscribeAllDataConsumers(self):
self.RefreshView()
--- a/graphics/DebugDataConsumer.py Sat Oct 12 10:10:30 2013 +0900
+++ b/graphics/DebugDataConsumer.py Mon Oct 14 10:31:32 2013 +0200
@@ -197,7 +197,7 @@
"""
self.DataType = data_type
- def NewValue(self, tick, value, forced=False, raw="BOOL"):
+ def NewValues(self, ticks, values, forced=False, raw="BOOL"):
"""
Function called by debug thread when a new debug value is available
@param tick: PLC tick when value was captured
@@ -205,6 +205,8 @@
@param forced: Forced flag, True if value is forced (default: False)
@param raw: Data type of values not translated (default: 'BOOL')
"""
+ tick, value = ticks[-1], values[-1]
+
# Translate value to IEC literal
if self.DataType != raw:
value = TYPE_TRANSLATOR.get(self.DataType, str)(value)