Laurent@814: #!/usr/bin/env python
Laurent@814: # -*- coding: utf-8 -*-
Laurent@814: 
andrej@1571: # This file is part of Beremiz, a Integrated Development Environment for
andrej@1571: # programming IEC 61131-3 automates supporting plcopen standard and CanFestival.
andrej@1571: #
andrej@1571: # Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
andrej@1571: #
andrej@1571: # See COPYING file for copyrights details.
andrej@1571: #
andrej@1571: # This program is free software; you can redistribute it and/or
andrej@1571: # modify it under the terms of the GNU General Public License
andrej@1571: # as published by the Free Software Foundation; either version 2
andrej@1571: # of the License, or (at your option) any later version.
andrej@1571: #
andrej@1571: # This program is distributed in the hope that it will be useful,
andrej@1571: # but WITHOUT ANY WARRANTY; without even the implied warranty of
andrej@1571: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
andrej@1571: # GNU General Public License for more details.
andrej@1571: #
andrej@1571: # You should have received a copy of the GNU General Public License
andrej@1571: # along with this program; if not, write to the Free Software
andrej@1571: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
Laurent@814: 
andrej@1881: 
andrej@1881: from __future__ import absolute_import
Laurent@1176: from threading import Lock, Timer
Laurent@1176: from time import time as gettime
Laurent@1176: 
Laurent@814: import wx
Laurent@1176: 
andrej@1737: REFRESH_PERIOD = 0.1         # Minimum time between 2 refresh
andrej@1737: DEBUG_REFRESH_LOCK = Lock()  # Common refresh lock for all debug viewers
Laurent@814: 
andrej@1782: # -------------------------------------------------------------------------------
Laurent@814: #                               Debug Viewer Class
andrej@1782: # -------------------------------------------------------------------------------
Laurent@814: 
Laurent@814: 
andrej@1831: class DebugViewer(object):
andrej@1736:     """
andrej@1736:     Class that implements common behavior of every viewers able to display debug
andrej@1736:     values
andrej@1736:     """
andrej@1730: 
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
andrej@1730: 
Laurent@1176:         # Flag indicating that consumer value update inhibited
Laurent@1176:         # (DebugViewer is refreshing)
Laurent@1176:         self.Inhibited = False
andrej@1730: 
Laurent@1176:         # List of data consumers subscribed to DataProducer
Laurent@1176:         self.DataConsumers = {}
andrej@1730: 
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()
andrej@1730: 
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()
andrej@1730: 
Laurent@1176:         # Set DataProducer and subscribe tick if needed
Laurent@1176:         self.SetDataProducer(producer)
andrej@1730: 
Laurent@1176:     def __del__(self):
Laurent@1176:         """
Laurent@1176:         Destructor
Laurent@1176:         """
Laurent@1176:         # Unsubscribe all data consumers
Laurent@1176:         self.UnsubscribeAllDataConsumers()
andrej@1730: 
Laurent@1176:         # Delete reference to DataProducer
Laurent@814:         self.DataProducer = None
andrej@1730: 
Laurent@1176:         # Stop last refresh timer
Laurent@877:         if self.LastRefreshTimer is not None:
Laurent@1176:             self.LastRefreshTimer.cancel()
andrej@1730: 
Laurent@1176:         # Release Common debug lock if DebugViewer has acquired it
Laurent@887:         if self.HasAcquiredLock:
Laurent@887:             DEBUG_REFRESH_LOCK.release()
andrej@1730: 
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:
andrej@1730: 
Laurent@1176:             # Subscribe tick to new data producer
Laurent@814:             if producer is not None:
Laurent@1365:                 producer.SubscribeDebugIECVariable("__tick__", self, True)
andrej@1730: 
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)
andrej@1730: 
Laurent@1176:         # Save new data producer
Laurent@814:         self.DataProducer = producer
andrej@1730: 
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
andrej@1730: 
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
andrej@1847:         for consumer, _iec_path in self.DataConsumers.iteritems():
Laurent@814:             consumer.Inhibit(inhibit)
andrej@1730: 
Laurent@1176:         # Save inhibit flag
Laurent@814:         self.Inhibited = inhibit
andrej@1730: 
Laurent@1365:     def AddDataConsumer(self, iec_path, consumer, buffer_list=False):
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
andrej@1730: 
Laurent@1176:         # Subscribe data consumer to DataProducer
Laurent@1176:         result = self.DataProducer.SubscribeDebugIECVariable(
andrej@1878:             iec_path, consumer, buffer_list)
Laurent@814:         if result is not None and consumer != self:
andrej@1730: 
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))
andrej@1730: 
Laurent@814:         return result
andrej@1730: 
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)
andrej@1730: 
Laurent@1176:         # Unsubscribe consumer from DataProducer
Laurent@814:         if iec_path is not None:
andrej@1878:             self.DataProducer.UnsubscribeDebugIECVariable(iec_path, consumer)
andrej@1730: 
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@1365:             self.DataProducer.SubscribeDebugIECVariable("__tick__", self, True)
andrej@1730: 
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:
andrej@1730: 
Laurent@1210:             # Unscribe tick if needed
Laurent@1210:             if self.SubscribeTick and tick and self.Debug:
Laurent@1210:                 self.DataProducer.UnsubscribeDebugIECVariable("__tick__", self)
andrej@1730: 
Laurent@1176:             # Unsubscribe all data consumers in list
Laurent@1176:             for consumer, iec_path in self.DataConsumers.iteritems():
andrej@1878:                 self.DataProducer.UnsubscribeDebugIECVariable(iec_path, consumer)
andrej@1730: 
Laurent@1176:         self.DataConsumers = {}
andrej@1730: 
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:
andrej@1730: 
Laurent@1176:             # Search for variable informations in project compilation files
Laurent@1176:             data_type = self.DataProducer.GetDebugIECVariableType(
andrej@1878:                 iec_path.upper())
Laurent@1102:             if data_type is not None:
Laurent@1102:                 return data_type
andrej@1730: 
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@1363:                 return infos.type
andrej@1730: 
Laurent@814:         return None
andrej@1730: 
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)
andrej@1730: 
Laurent@1176:         return False
andrej@1730: 
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)
andrej@1730: 
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)
andrej@1730: 
Edouard@1431:     def NewDataAvailable(self, ticks):
Laurent@1176:         """
Laurent@1215:         Called by DataProducer for each tick captured
Laurent@1176:         @param tick: PLC tick captured
andrej@1730:         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()
andrej@1742:             self.LastRefreshTimer = None
Laurent@877:         self.TimerAccessLock.release()
andrej@1730: 
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:
andrej@1730: 
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):
Edouard@1431:                 self.StartRefreshing()
andrej@1730: 
Laurent@1176:             # If common lock wasn't acquired for any reason, restart last
Laurent@1176:             # refresh timer
Laurent@933:             else:
Edouard@1431:                 self.StartLastRefreshTimer()
andrej@1730: 
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)
andrej@1730: 
Edouard@1431:     def ShouldRefresh(self):
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():
andrej@1730: 
Laurent@1176:             # Try to acquire common refresh lock
Laurent@1176:             if DEBUG_REFRESH_LOCK.acquire(False):
Edouard@1431:                 self.StartRefreshing()
andrej@1730: 
Laurent@1176:             # Restart last refresh timer if common refresh lock acquired failed
Laurent@1176:             else:
Edouard@1431:                 self.StartLastRefreshTimer()
andrej@1730: 
Edouard@1431:     def StartRefreshing(self):
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()
andrej@1730: 
Laurent@1176:         # Inhibit data consumer value update
Laurent@1176:         self.Inhibit(True)
andrej@1730: 
Laurent@1176:         # Initiate DebugViewer refresh
Edouard@1431:         wx.CallAfter(self.RefreshNewData)
andrej@1730: 
Edouard@1431:     def StartLastRefreshTimer(self):
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(
Edouard@1431:             REFRESH_PERIOD, self.ShouldRefresh)
Laurent@1176:         self.LastRefreshTimer.start()
Laurent@1176:         self.TimerAccessLock.release()
andrej@1730: 
Edouard@1431:     def RefreshNewData(self):
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
andrej@1730:         class
Laurent@1176:         """
Laurent@814:         if self:
Laurent@1176:             # Activate data consumer value update
Laurent@1176:             self.Inhibit(False)
andrej@1730: 
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()