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) 2012: 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@887: import numpy
Laurent@924: import binascii
Laurent@814: 
Laurent@1193: from graphics.DebugDataConsumer import DebugDataConsumer, TYPE_TRANSLATOR
Laurent@1193: 
Laurent@1193: #-------------------------------------------------------------------------------
Laurent@1193: #                 Constant for calculate CRC for string variables
Laurent@1193: #-------------------------------------------------------------------------------
Laurent@1193: 
Laurent@1193: STRING_CRC_SIZE = 8
Laurent@1193: STRING_CRC_MASK = 2 ** STRING_CRC_SIZE - 1
Laurent@1193: 
Laurent@1193: #-------------------------------------------------------------------------------
Laurent@1193: #                          Debug Variable Item Class
Laurent@1193: #-------------------------------------------------------------------------------
Laurent@1193: 
Laurent@1193: """
Laurent@1193: Class that implements an element that consumes debug values for PLC variable and
Laurent@1193: stores received values for displaying them in graphic panel or table
Laurent@1193: """
Laurent@1193: 
Laurent@1193: class DebugVariableItem(DebugDataConsumer):
Laurent@1193:     
Laurent@1193:     def __init__(self, parent, variable, store_data=False):
Laurent@1193:         """
Laurent@1193:         Constructor
Laurent@1193:         @param parent: Reference to debug variable panel
Laurent@1193:         @param variable: Path of variable to debug
Laurent@1193:         """
Laurent@814:         DebugDataConsumer.__init__(self)
Laurent@1193:         
Laurent@814:         self.Parent = parent
Laurent@814:         self.Variable = variable
Laurent@1193:         self.StoreData = store_data
Laurent@1193:         
Laurent@1193:         # Get Variable data type
Laurent@887:         self.RefreshVariableType()
Laurent@909:         
Laurent@814:     def __del__(self):
Laurent@1193:         """
Laurent@1193:         Destructor
Laurent@1193:         """
Laurent@1193:         # Reset reference to debug variable panel
Laurent@814:         self.Parent = None
Laurent@814:     
Laurent@814:     def SetVariable(self, variable):
Laurent@1193:         """
Laurent@1193:         Set path of variable 
Laurent@1193:         @param variable: Path of variable to debug
Laurent@1193:         """
Laurent@1193:         if self.Parent is not None and self.Variable != variable:
Laurent@1193:             # Store variable path
Laurent@814:             self.Variable = variable
Laurent@1193:             # Get Variable data type
Laurent@887:             self.RefreshVariableType()
Laurent@1193:             
Laurent@1193:             # Refresh debug variable panel
Laurent@916:             self.Parent.RefreshView()
Laurent@916:     
Laurent@924:     def GetVariable(self, mask=None):
Laurent@1193:         """
Laurent@1193:         Return path of variable 
Laurent@1193:         @param mask: Mask to apply to variable path [var_name, '*',...]
Laurent@1193:         @return: String containing masked variable path
Laurent@1193:         """
Laurent@1193:         # Apply mask to variable name
Laurent@924:         if mask is not None:
Laurent@1193:             # '#' correspond to parts that are different between all items
Laurent@1193:             
Laurent@1193:             # Extract variable path parts
Laurent@1193:             parts = self.Variable.split('.')
Laurent@1193:             # Adjust mask size to size of variable path
Laurent@924:             mask = mask + ['*'] * max(0, len(parts) - len(mask))
Laurent@1193:             
Laurent@1193:             # Store previous mask
Laurent@924:             last = None
Laurent@1193:             # Init masked variable path
Laurent@924:             variable = ""
Laurent@1193:             
Laurent@1193:             for m, p in zip(mask, parts):
Laurent@1193:                 # Part is not masked, add part prefixed with '.' is previous
Laurent@1193:                 # wasn't masked
Laurent@924:                 if m == '*':
Laurent@1193:                     variable += ('.' if last == '*' else '') + p
Laurent@1193:                 
Laurent@1193:                 # Part is mask, add '..' if first or previous wasn't masked
Laurent@924:                 elif last is None or last == '*':
Laurent@924:                     variable += '..'
Laurent@1193:                 
Laurent@924:                 last = m
Laurent@1193:             
Laurent@1193:             return variable
Laurent@1193:         
Laurent@1193:         return self.Variable
Laurent@814:     
Laurent@887:     def RefreshVariableType(self):
Laurent@1193:         """
Laurent@1193:         Get and store variable data type
Laurent@1193:         """
Laurent@887:         self.VariableType = self.Parent.GetDataType(self.Variable)
Laurent@1193:         # Reset data stored
Laurent@1193:         self.ResetData()
Laurent@887:     
Laurent@887:     def GetVariableType(self):
Laurent@1193:         """
Laurent@1193:         Return variable data type
Laurent@1193:         @return: Variable data type
Laurent@1193:         """
Laurent@887:         return self.VariableType
Laurent@887:     
Laurent@902:     def GetData(self, start_tick=None, end_tick=None):
Laurent@1193:         """
Laurent@1193:         Return data stored contained in given range
Laurent@1193:         @param start_tick: Start tick of given range (default None, first data)
Laurent@1193:         @param end_tick: end tick of given range (default None, last data)
Laurent@1193:         @return: Data as numpy.array([(tick, value, forced),...])
Laurent@1193:         """
Laurent@1193:         # Return immediately if data empty or none
Laurent@1193:         if self.Data is None or len(self.Data) == 0:
Laurent@1193:             return self.Data
Laurent@1193:         
Laurent@1193:         # Find nearest data outside given range indexes
Laurent@1193:         start_idx = (self.GetNearestData(start_tick, -1)
Laurent@1193:                      if start_tick is not None
Laurent@1193:                      else 0)
Laurent@1193:         end_idx = (self.GetNearestData(end_tick, 1)
Laurent@1193:                    if end_tick is not None
Laurent@1193:                    else len(self.Data))
Laurent@1193:         
Laurent@1193:         # Return data between indexes
Laurent@1193:         return self.Data[start_idx:end_idx]
Laurent@1193:     
Laurent@1193:     def GetRawValue(self, index):
Laurent@1193:         """
Laurent@1193:         Return raw value at given index for string variables
Laurent@1193:         @param index: Variable value index
Laurent@1193:         @return: Variable data type
Laurent@1193:         """
Laurent@1193:         if (self.VariableType in ["STRING", "WSTRING"] and
Laurent@1193:             index < len(self.RawData)):
Laurent@1268:             return self.RawData[index][0]
Laurent@924:         return ""
Laurent@924:     
Laurent@1193:     def GetValueRange(self):
Laurent@1193:         """
Laurent@1193:         Return variable value range
Laurent@1193:         @return: (minimum_value, maximum_value)
Laurent@1193:         """
Laurent@902:         return self.MinValue, self.MaxValue
Laurent@887:     
Laurent@1267:     def GetDataAndValueRange(self, start_tick, end_tick, full_range=True):
Laurent@1267:         """
Laurent@1267:         Return variable data and value range for a given tick range
Laurent@1215:         @param start_tick: Start tick of given range (default None, first data)
Laurent@1215:         @param end_tick: end tick of given range (default None, last data)
Laurent@1267:         @param full_range: Value range is calculated on whole data (False: only
Laurent@1267:         calculated on data in given range)
Laurent@1215:         @return: (numpy.array([(tick, value, forced),...]), 
Laurent@1215:                   min_value, max_value)
Laurent@1215:         """
Laurent@1267:         # Get data in given tick range
Laurent@1267:         data = self.GetData(start_tick, end_tick)
Laurent@1267:         
Laurent@1267:         # Value range is calculated on whole data
Laurent@1267:         if full_range:
Laurent@1267:             return data, self.MinValue, self.MaxValue
Laurent@1267:         
Laurent@1267:         # Check that data in given range is not empty
Laurent@1267:         values = data[:, 1]
Laurent@1267:         if len(values) > 0:
Laurent@1267:             # Return value range for data in given tick range
Laurent@1267:             return (data,
Laurent@1267:                     data[numpy.argmin(values), 1],
Laurent@1267:                     data[numpy.argmax(values), 1])
Laurent@1267:         
Laurent@1267:         # Return default values
Laurent@1267:         return data, None, None
Laurent@1215:     
Laurent@887:     def ResetData(self):
Laurent@1193:         """
Laurent@1193:         Reset data stored when store data option enabled
Laurent@1193:         """
Laurent@1193:         if self.StoreData and self.IsNumVariable():
Laurent@1193:             # Init table storing data
Laurent@924:             self.Data = numpy.array([]).reshape(0, 3)
Laurent@1193:             
Laurent@1193:             # Init table storing raw data if variable is strin
Laurent@1193:             self.RawData = ([]
Laurent@1193:                             if self.VariableType in ["STRING", "WSTRING"]
Laurent@1193:                             else None)
Laurent@1193:                 
Laurent@1193:             # Init Value range variables
Laurent@902:             self.MinValue = None
Laurent@902:             self.MaxValue = None
Laurent@1193:         
Laurent@887:         else:
Laurent@887:             self.Data = None
Laurent@1193:         
Laurent@1193:         # Init variable value
Laurent@1040:         self.Value = ""
Laurent@887:     
Laurent@887:     def IsNumVariable(self):
Laurent@1193:         """
Laurent@1193:         Return if variable data type is numeric. String variables are
Laurent@1193:         considered as numeric (string CRC)
Laurent@1193:         @return: True if data type is numeric
Laurent@1193:         """
Laurent@924:         return (self.Parent.IsNumType(self.VariableType) or 
Laurent@924:                 self.VariableType in ["STRING", "WSTRING"])
Laurent@887:     
Laurent@1365:     def NewValues(self, ticks, values):
Laurent@1193:         """
Laurent@1193:         Function called by debug thread when a new debug value is available
Laurent@1193:         @param tick: PLC tick when value was captured
Laurent@1193:         @param value: Value captured
Laurent@1193:         @param forced: Forced flag, True if value is forced (default: False)
Laurent@1193:         """
Laurent@1365:         DebugDataConsumer.NewValues(self, ticks[-1], values[-1], raw=None)
Laurent@1193:         
Laurent@1193:         if self.Data is not None:
Laurent@1363:             
Laurent@1363:             if self.VariableType in ["STRING", "WSTRING"]:
Laurent@1363:                 last_raw_data = (self.RawData[-1]
Laurent@1363:                                  if len(self.RawData) > 0 else None)
Laurent@1363:                 last_raw_data_idx = len(self.RawData) - 1
Laurent@1193:             
Laurent@1363:             data_values = []
Laurent@1365:             for tick, (value, forced) in zip(ticks, values):
Laurent@1365:                 # Translate forced flag to float for storing in Data table
Laurent@1365:                 forced_value = float(forced)
Laurent@1365:                 
Laurent@1363:                 # String data value is CRC
Laurent@1363:                 num_value = (binascii.crc32(value) & STRING_CRC_MASK
Laurent@1363:                              if self.VariableType in ["STRING", "WSTRING"]
Laurent@1363:                              else float(value))
Laurent@1363:             
Laurent@1363:                 # Update variable range values
Laurent@1363:                 self.MinValue = (min(self.MinValue, num_value)
Laurent@1363:                                  if self.MinValue is not None
Laurent@1363:                                  else num_value)
Laurent@1363:                 self.MaxValue = (max(self.MaxValue, num_value)
Laurent@1363:                                  if self.MaxValue is not None
Laurent@1363:                                  else num_value)
Laurent@1363:             
Laurent@1363:                 # In the case of string variables, we store raw string value and
Laurent@1363:                 # forced flag in raw data table. Only changes in this two values
Laurent@1363:                 # are stored. Index to the corresponding raw value is stored in 
Laurent@1363:                 # data third column
Laurent@1363:                 if self.VariableType in ["STRING", "WSTRING"]:
Laurent@1363:                     raw_data = (value, forced_value)
Laurent@1363:                     if len(self.RawData) == 0 or last_raw_data != raw_data:
Laurent@1363:                         last_raw_data_idx += 1
Laurent@1363:                         last_raw_data = raw_data
Laurent@1363:                         self.RawData.append(raw_data)
Laurent@1363:                     extra_value = last_raw_data_idx
Laurent@1363:                 
Laurent@1363:                 # In other case, data third column is forced flag
Laurent@924:                 else:
Laurent@1363:                     extra_value = forced_value
Laurent@1363:             
Laurent@1363:                 data_values.append(
Laurent@1363:                     [float(tick), num_value, extra_value])
Laurent@1193:             
Laurent@1193:             # Add New data to stored data table
Laurent@1363:             self.Data = numpy.append(self.Data, data_values, axis=0)
Laurent@1363:             
Laurent@1193:             # Signal to debug variable panel to refresh
Laurent@887:             self.Parent.HasNewData = True
Laurent@1193:         
Laurent@814:     def SetForced(self, forced):
Laurent@1193:         """
Laurent@1193:         Update Forced flag
Laurent@1193:         @param forced: New forced flag
Laurent@1193:         """
Laurent@1193:         # Store forced flag
Laurent@814:         if self.Forced != forced:
Laurent@814:             self.Forced = forced
Laurent@1193:             
Laurent@1193:             # Signal to debug variable panel to refresh
Laurent@814:             self.Parent.HasNewData = True
Laurent@814:     
Laurent@814:     def SetValue(self, value):
Laurent@1193:         """
Laurent@1193:         Update value.
Laurent@1193:         @param value: New value
Laurent@1193:         """
Laurent@1193:         # Remove quote and double quote surrounding string value to get raw value
Laurent@1193:         if (self.VariableType == "STRING" and
Laurent@1193:             value.startswith("'") and value.endswith("'") or
Laurent@1193:             self.VariableType == "WSTRING" and
Laurent@1193:             value.startswith('"') and value.endswith('"')):
Laurent@892:             value = value[1:-1]
Laurent@1193:         
Laurent@1193:         # Store variable value
Laurent@814:         if self.Value != value:
Laurent@814:             self.Value = value
Laurent@1193:             
Laurent@1193:             # Signal to debug variable panel to refresh
Laurent@814:             self.Parent.HasNewData = True
Laurent@1193:     
Laurent@924:     def GetValue(self, tick=None, raw=False):
Laurent@1193:         """
Laurent@1193:         Return current value or value and forced flag for tick given
Laurent@1193:         @return: Current value or value and forced flag
Laurent@1193:         """
Laurent@1193:         # If tick given and stored data option enabled
Laurent@1193:         if tick is not None and self.Data is not None:
Laurent@1193:             
Laurent@1193:             # Return current value and forced flag if data empty
Laurent@1193:             if len(self.Data) == 0:
Laurent@1193:                 return self.Value, self.IsForced()
Laurent@1193:             
Laurent@1193:             # Get index of nearest data from tick given
Laurent@1193:             idx = self.GetNearestData(tick, 0)
Laurent@1193:             
Laurent@1193:             # Get value and forced flag at given index
Laurent@1193:             value, forced = self.RawData[int(self.Data[idx, 2])] \
Laurent@1193:                             if self.VariableType in ["STRING", "WSTRING"] \
Laurent@1193:                             else self.Data[idx, 1:3]
Laurent@1193:             
Laurent@1193:             # Get raw value if asked
Laurent@1193:             if not raw:
Laurent@1193:                 value = TYPE_TRANSLATOR.get(
Laurent@1193:                         self.VariableType, str)(value)
Laurent@1193:             
Laurent@1193:             return value, forced
Laurent@1193:             
Laurent@1193:         # Return raw value if asked
Laurent@1193:         if not raw and self.VariableType in ["STRING", "WSTRING"]:
Laurent@1193:             return TYPE_TRANSLATOR.get(
Laurent@1193:                     self.VariableType, str)(self.Value)
Laurent@814:         return self.Value
Laurent@814: 
Laurent@902:     def GetNearestData(self, tick, adjust):
Laurent@1193:         """
Laurent@1193:         Return index of nearest data from tick given
Laurent@1193:         @param tick: Tick where find nearest data
Laurent@1193:         @param adjust: Constraint for data position from tick
Laurent@1193:                        -1: older than tick
Laurent@1193:                        1:  newer than tick
Laurent@1193:                        0:  doesn't matter
Laurent@1193:         @return: Index of nearest data
Laurent@1193:         """
Laurent@1193:         # Return immediately if data is empty
Laurent@1193:         if self.Data is None:
Laurent@916:             return None
Laurent@916:         
Laurent@1193:         # Extract data ticks
Laurent@1193:         ticks = self.Data[:, 0]
Laurent@1193:         
Laurent@1193:         # Get nearest data from tick
Laurent@1193:         idx = numpy.argmin(abs(ticks - tick))
Laurent@1193:         
Laurent@1193:         # Adjust data index according to constraint
Laurent@1193:         if (adjust < 0 and ticks[idx] > tick and idx > 0 or
Laurent@1193:             adjust > 0 and ticks[idx] < tick and idx < len(ticks)):
Laurent@1193:             idx += adjust
Laurent@1193:         
Laurent@1193:         return idx