editors/DebugViewer.py
changeset 1176 f4b434672204
parent 1173 ad09b4a755ce
child 1205 638d1d430d24
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/editors/DebugViewer.py	Mon May 27 13:09:54 2013 +0200
@@ -0,0 +1,348 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+#This file is part of PLCOpenEditor, a library implementing an IEC 61131-3 editor
+#based on the plcopen standard. 
+#
+#Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
+#
+#See COPYING file for copyrights details.
+#
+#This library is free software; you can redistribute it and/or
+#modify it under the terms of the GNU General Public
+#License as published by the Free Software Foundation; either
+#version 2.1 of the License, or (at your option) any later version.
+#
+#This library is distributed in the hope that it will be useful,
+#but WITHOUT ANY WARRANTY; without even the implied warranty of
+#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#General Public License for more details.
+#
+#You should have received a copy of the GNU General Public
+#License along with this library; if not, write to the Free Software
+#Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from threading import Lock, Timer
+from time import time as gettime
+
+import wx
+
+REFRESH_PERIOD = 0.1        # Minimum time between 2 refresh
+DEBUG_REFRESH_LOCK = Lock() # Common refresh lock for all debug viewers
+
+#-------------------------------------------------------------------------------
+#                               Debug Viewer Class
+#-------------------------------------------------------------------------------
+
+"""
+Class that implements common behavior of every viewers able to display debug 
+values
+"""
+
+class DebugViewer:
+    
+    def __init__(self, producer, debug, subscribe_tick=True):
+        """
+        Constructor
+        @param producer: Object receiving debug value and dispatching them to
+        consumers
+        @param debug: Flag indicating that Viewer is debugging
+        @param subscribe_tick: Flag indicating that viewer need tick value to
+        synchronize
+        """
+        self.Debug = debug
+        self.SubscribeTick = subscribe_tick
+        
+        # Flag indicating that consumer value update inhibited
+        # (DebugViewer is refreshing)
+        self.Inhibited = False
+        
+        # List of data consumers subscribed to DataProducer
+        self.DataConsumers = {}
+        
+        # Time stamp indicating when last refresh have been initiated
+        self.LastRefreshTime = gettime()
+        # Flag indicating that DebugViewer has acquire common debug lock
+        self.HasAcquiredLock = False
+        # Lock for access to the two preceding variable
+        self.AccessLock = Lock()
+        
+        # Timer to refresh Debug Viewer one last time in the case that a new
+        # value have been received during refresh was inhibited and no one
+        # after refresh was activated
+        self.LastRefreshTimer = None
+        # Lock for access to the timer
+        self.TimerAccessLock = Lock()
+        
+        # Set DataProducer and subscribe tick if needed
+        self.SetDataProducer(producer)
+        
+    def __del__(self):
+        """
+        Destructor
+        """
+        # Unsubscribe all data consumers
+        self.UnsubscribeAllDataConsumers()
+        
+        # Delete reference to DataProducer
+        self.DataProducer = None
+        
+        # Stop last refresh timer
+        if self.LastRefreshTimer is not None:
+            self.LastRefreshTimer.cancel()
+        
+        # Release Common debug lock if DebugViewer has acquired it
+        if self.HasAcquiredLock:
+            DEBUG_REFRESH_LOCK.release()
+    
+    def SetDataProducer(self, producer):
+        """
+        Set Data Producer
+        @param producer: Data Producer
+        """
+        # In the case that tick need to be subscribed and DebugViewer is
+        # debugging
+        if self.SubscribeTick and self.Debug:
+            
+            # Subscribe tick to new data producer
+            if producer is not None:
+                producer.SubscribeDebugIECVariable("__tick__", self)
+            
+            # Unsubscribe tick from old data producer
+            if getattr(self, "DataProducer", None) is not None:
+                self.DataProducer.UnsubscribeDebugIECVariable("__tick__", self)
+        
+        # Save new data producer
+        self.DataProducer = producer
+    
+    def IsDebugging(self):
+        """
+        Get flag indicating if Debug Viewer is debugging
+        @return: Debugging flag
+        """
+        return self.Debug
+    
+    def Inhibit(self, inhibit):
+        """
+        Set consumer value update inhibit flag
+        @param inhibit: Inhibit flag
+        """
+        # Inhibit every data consumers in list
+        for consumer, iec_path in self.DataConsumers.iteritems():
+            consumer.Inhibit(inhibit)
+        
+        # Save inhibit flag
+        self.Inhibited = inhibit
+    
+    def AddDataConsumer(self, iec_path, consumer):
+        """
+        Subscribe data consumer to DataProducer
+        @param iec_path: Path in PLC of variable needed by data consumer
+        @param consumer: Data consumer to subscribe
+        @return: List of value already received [(tick, data),...] (None if
+        subscription failed)
+        """
+        # Return immediately if no DataProducer defined
+        if self.DataProducer is None:
+            return None
+        
+        # Subscribe data consumer to DataProducer
+        result = self.DataProducer.SubscribeDebugIECVariable(
+                        iec_path, consumer)
+        if result is not None and consumer != self:
+            
+            # Store data consumer if successfully subscribed and inform
+            # consumer of variable data type
+            self.DataConsumers[consumer] = iec_path
+            consumer.SetDataType(self.GetDataType(iec_path))
+        
+        return result
+    
+    def RemoveDataConsumer(self, consumer):
+        """
+        Unsubscribe data consumer from DataProducer
+        @param consumer: Data consumer to unsubscribe
+        """
+        # Remove consumer from data consumer list
+        iec_path = self.DataConsumers.pop(consumer, None)
+        
+        # Unsubscribe consumer from DataProducer
+        if iec_path is not None:
+            self.DataProducer.UnsubscribeDebugIECVariable(
+                        iec_path, consumer)
+    
+    def SubscribeAllDataConsumers(self):
+        """
+        Called to Subscribe all data consumers contained in DebugViewer.
+        May be overridden by inherited classes.
+        """
+        # Subscribe tick if needed
+        if self.SubscribeTick and self.Debug and self.DataProducer is not None:
+            self.DataProducer.SubscribeDebugIECVariable("__tick__", self)
+    
+    def UnsubscribeAllDataConsumers(self):
+        """
+        Called to Unsubscribe all data consumers.
+        """
+        if self.DataProducer is not None:
+            
+            # Unsubscribe all data consumers in list
+            for consumer, iec_path in self.DataConsumers.iteritems():
+                self.DataProducer.UnsubscribeDebugIECVariable(
+                            iec_path, consumer)
+            
+            # Unscribe tick if needed
+            if self.SubscribeTick and self.Debug:
+                self.DataProducer.UnsubscribeDebugIECVariable("__tick__", self)
+        
+        self.DataConsumers = {}
+    
+    def GetDataType(self, iec_path):
+        """
+        Return variable data type.
+        @param iec_path: Path in PLC of variable
+        @return: variable data type (None if not found)
+        """
+        if self.DataProducer is not None:
+            
+            # Search for variable informations in project compilation files
+            data_type = self.DataProducer.GetDebugIECVariableType(
+                            iec_path.upper())
+            if data_type is not None:
+                return data_type
+            
+            # Search for variable informations in project data
+            infos = self.DataProducer.GetInstanceInfos(iec_path)
+            if infos is not None:
+                return infos["type"]
+        
+        return None
+    
+    def IsNumType(self, data_type):
+        """
+        Indicate if data type given is a numeric data type
+        @param data_type: Data type to test
+        @return: True if data type given is numeric
+        """
+        if self.DataProducer is not None:
+            return self.DataProducer.IsNumType(data_type)
+        
+        return False
+    
+    def ForceDataValue(self, iec_path, value):
+        """
+        Force PLC variable value
+        @param iec_path: Path in PLC of variable to force
+        @param value: Value forced
+        """
+        if self.DataProducer is not None:
+            self.DataProducer.ForceDebugIECVariable(iec_path, value)
+    
+    def ReleaseDataValue(self, iec_path):
+        """
+        Release PLC variable value
+        @param iec_path: Path in PLC of variable to release
+        """
+        if self.DataProducer is not None:
+            self.DataProducer.ReleaseDebugIECVariable(iec_path)
+    
+    def NewDataAvailable(self, tick, *args, **kwargs):
+        """
+        Called by DataProducer for each tick captured in 
+        @param tick: PLC tick captured
+        All other parameters are passed to refresh function 
+        """
+        # Stop last refresh timer
+        self.TimerAccessLock.acquire()
+        if self.LastRefreshTimer is not None:
+            self.LastRefreshTimer.cancel()
+            self.LastRefreshTimer=None
+        self.TimerAccessLock.release()
+        
+        # Only try to refresh DebugViewer if it is visible on screen and not
+        # already refreshing
+        if self.IsShown() and not self.Inhibited:
+            
+            # Try to get acquire common refresh lock if minimum period between
+            # two refresh has expired
+            if gettime() - self.LastRefreshTime > REFRESH_PERIOD and \
+               DEBUG_REFRESH_LOCK.acquire(False):
+                self.StartRefreshing(*args, **kwargs)
+            
+            # If common lock wasn't acquired for any reason, restart last
+            # refresh timer
+            else:
+                self.StartLastRefreshTimer(*args, **kwargs)
+        
+        # In the case that DebugViewer isn't visible on screen and has already
+        # acquired common refresh lock, reset DebugViewer
+        elif not self.IsShown() and self.HasAcquiredLock:
+            DebugViewer.RefreshNewData(self)
+    
+    def ShouldRefresh(self, *args, **kwargs):
+        """
+        Callback function called when last refresh timer expired
+        All parameters are passed to refresh function
+        """
+        # Cancel if DebugViewer is not visible on screen
+        if self and self.IsShown():
+            
+            # Try to acquire common refresh lock
+            if DEBUG_REFRESH_LOCK.acquire(False):
+                self.StartRefreshing(*args, **kwargs)
+            
+            # Restart last refresh timer if common refresh lock acquired failed
+            else:
+                self.StartLastRefreshTimer(*args, **kwargs)
+    
+    def StartRefreshing(self, *args, **kwargs):
+        """
+        Called to initiate a refresh of DebugViewer
+        All parameters are passed to refresh function
+        """
+        # Update last refresh time stamp and flag for common refresh
+        # lock acquired
+        self.AccessLock.acquire()
+        self.HasAcquiredLock = True
+        self.LastRefreshTime = gettime()
+        self.AccessLock.release()
+        
+        # Inhibit data consumer value update
+        self.Inhibit(True)
+        
+        # Initiate DebugViewer refresh
+        wx.CallAfter(self.RefreshNewData, *args, **kwargs)
+    
+    def StartLastRefreshTimer(self, *args, **kwargs):
+        """
+        Called to start last refresh timer for the minimum time between 2
+        refresh
+        All parameters are passed to refresh function
+        """
+        self.TimerAccessLock.acquire()
+        self.LastRefreshTimer = Timer(
+            REFRESH_PERIOD, self.ShouldRefresh, args, kwargs)
+        self.LastRefreshTimer.start()
+        self.TimerAccessLock.release()
+    
+    def RefreshNewData(self, *args, **kwargs):
+        """
+        Called to refresh DebugViewer according to values received by data
+        consumers
+        May be overridden by inherited classes
+        Can receive any parameters depending on what is needed by inherited
+        class 
+        """
+        if self:
+            # Activate data consumer value update
+            self.Inhibit(False)
+            
+            # Release common refresh lock if acquired and update
+            # last refresh time
+            self.AccessLock.acquire()
+            if self.HasAcquiredLock:
+                DEBUG_REFRESH_LOCK.release()
+                self.HasAcquiredLock = False
+            if gettime() - self.LastRefreshTime > REFRESH_PERIOD:
+                self.LastRefreshTime = gettime()
+            self.AccessLock.release()