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