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) 2012: 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
surkovsv93@1966: from datetime import timedelta
andrej@1832: import binascii
Edouard@2741: import numpy as np
Laurent@1193: from graphics.DebugDataConsumer import DebugDataConsumer, TYPE_TRANSLATOR
Edouard@2741: from controls.DebugVariablePanel.RingBuffer import RingBuffer
Laurent@1193: 
andrej@1782: # -------------------------------------------------------------------------------
Laurent@1193: #                 Constant for calculate CRC for string variables
andrej@1782: # -------------------------------------------------------------------------------
Laurent@1193: 
Laurent@1193: STRING_CRC_SIZE = 8
Laurent@1193: STRING_CRC_MASK = 2 ** STRING_CRC_SIZE - 1
Laurent@1193: 
andrej@1782: # -------------------------------------------------------------------------------
Laurent@1193: #                          Debug Variable Item Class
andrej@1782: # -------------------------------------------------------------------------------
Laurent@1193: 
Laurent@1193: 
Laurent@1193: class DebugVariableItem(DebugDataConsumer):
andrej@1736:     """
andrej@1736:     Class that implements an element that consumes debug values for PLC variable and
andrej@1736:     stores received values for displaying them in graphic panel or table
andrej@1736:     """
andrej@1730: 
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)
andrej@1730: 
Laurent@814:         self.Parent = parent
Laurent@814:         self.Variable = variable
Laurent@1193:         self.StoreData = store_data
andrej@1730: 
Laurent@1193:         # Get Variable data type
Laurent@887:         self.RefreshVariableType()
andrej@1730: 
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
andrej@1730: 
Laurent@814:     def SetVariable(self, variable):
Laurent@1193:         """
andrej@1730:         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()
andrej@1730: 
Laurent@1193:             # Refresh debug variable panel
Laurent@916:             self.Parent.RefreshView()
andrej@1730: 
Laurent@924:     def GetVariable(self, mask=None):
Laurent@1193:         """
andrej@1730:         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
andrej@1730: 
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))
andrej@1730: 
Laurent@1193:             # Store previous mask
Laurent@924:             last = None
Laurent@1193:             # Init masked variable path
Laurent@924:             variable = ""
andrej@1730: 
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
andrej@1730: 
Laurent@1193:                 # Part is mask, add '..' if first or previous wasn't masked
Laurent@924:                 elif last is None or last == '*':
Laurent@924:                     variable += '..'
andrej@1730: 
Laurent@924:                 last = m
andrej@1730: 
Laurent@1193:             return variable
andrej@1730: 
Laurent@1193:         return self.Variable
andrej@1730: 
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()
andrej@1730: 
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
andrej@1730: 
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
Edouard@2741:         if self.Data is None or self.Data.count == 0:
Edouard@3335:             return []
andrej@1730: 
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
edouard@3292:                    else self.Data.count)
andrej@1730: 
Laurent@1193:         # Return data between indexes
Edouard@2741:         return self.Data.view[start_idx:end_idx]
andrej@1730: 
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:         """
andrej@1766:         if self.VariableType in ["STRING", "WSTRING"] and index < len(self.RawData):
Laurent@1268:             return self.RawData[index][0]
Laurent@924:         return ""
andrej@1730: 
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
andrej@1730: 
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)
andrej@1730:         @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)
andrej@1730: 
Laurent@1267:         # Value range is calculated on whole data
Laurent@1267:         if full_range:
Laurent@1267:             return data, self.MinValue, self.MaxValue
andrej@1730: 
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,
Edouard@2741:                     data[np.argmin(values), 1],
Edouard@2741:                     data[np.argmax(values), 1])
andrej@1730: 
Laurent@1267:         # Return default values
Laurent@1267:         return data, None, None
andrej@1730: 
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
Edouard@2741:             self.Data = RingBuffer(3)
andrej@1730: 
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)
andrej@1730: 
Laurent@1193:             # Init Value range variables
Laurent@902:             self.MinValue = None
Laurent@902:             self.MaxValue = None
andrej@1730: 
Laurent@887:         else:
Laurent@887:             self.Data = None
surkovsv93@1962:             self.MinValue = None
surkovsv93@1962:             self.MaxValue = None
Laurent@1193:         # Init variable value
Laurent@1040:         self.Value = ""
andrej@1730: 
Laurent@887:     def IsNumVariable(self):
Laurent@1193:         """
Laurent@1193:         Return if variable data type is numeric. String variables are
surkovsv93@1962:         considered as numeric (string CRC). Time variables are considered
surkovsv93@1962:         as number of seconds
Laurent@1193:         @return: True if data type is numeric
Laurent@1193:         """
andrej@1730:         return (self.Parent.IsNumType(self.VariableType) or
surkovsv93@1962:                 self.VariableType in ["STRING", "WSTRING", "TIME", "TOD", "DT", "DATE"])
andrej@1730: 
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)
andrej@1730: 
Laurent@1193:         if self.Data is not None:
andrej@1730: 
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
andrej@1730: 
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)
andrej@1730: 
surkovsv93@1962:                 if self.VariableType in ["STRING", "WSTRING"]:
surkovsv93@1962:                     # String data value is CRC
surkovsv93@1962:                     num_value = (binascii.crc32(value) & STRING_CRC_MASK)
surkovsv93@1962:                 elif self.VariableType in ["TIME", "TOD", "DT", "DATE"]:
surkovsv93@1962:                     # Numeric value of time type variables
surkovsv93@1962:                     # is represented in seconds
surkovsv93@1962:                     num_value = float(value.total_seconds())
surkovsv93@1962:                 else:
surkovsv93@1962:                     num_value = float(value)
andrej@1730: 
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)
andrej@1730: 
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
andrej@1730:                 # 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
andrej@1730: 
Laurent@1363:                 # In other case, data third column is forced flag
Laurent@924:                 else:
Laurent@1363:                     extra_value = forced_value
andrej@1730: 
Laurent@1363:                 data_values.append(
Laurent@1363:                     [float(tick), num_value, extra_value])
andrej@1730: 
Laurent@1193:             # Add New data to stored data table
Edouard@2741:             self.Data.append(data_values)
andrej@1730: 
Laurent@1193:             # Signal to debug variable panel to refresh
Laurent@887:             self.Parent.HasNewData = True
andrej@1730: 
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
andrej@1730: 
Laurent@1193:             # Signal to debug variable panel to refresh
Laurent@814:             self.Parent.HasNewData = True
andrej@1730: 
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
andrej@1774:         if self.VariableType == "STRING" and value.startswith("'") and value.endswith("'") or \
andrej@1766:            self.VariableType == "WSTRING" and value.startswith('"') and value.endswith('"'):
Laurent@892:             value = value[1:-1]
andrej@1730: 
Laurent@1193:         # Store variable value
Laurent@814:         if self.Value != value:
Laurent@814:             self.Value = value
andrej@1730: 
Laurent@1193:             # Signal to debug variable panel to refresh
Laurent@814:             self.Parent.HasNewData = True
andrej@1730: 
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:
andrej@1730: 
Laurent@1193:             # Return current value and forced flag if data empty
Edouard@2741:             if self.Data.count == 0:
Laurent@1193:                 return self.Value, self.IsForced()
andrej@1730: 
Laurent@1193:             # Get index of nearest data from tick given
Laurent@1193:             idx = self.GetNearestData(tick, 0)
andrej@1730: 
Laurent@1193:             # Get value and forced flag at given index
andrej@1767:             value, forced = \
Edouard@2741:                 self.RawData[int(self.Data.view[idx, 2])] \
andrej@1767:                 if self.VariableType in ["STRING", "WSTRING"] \
Edouard@2741:                 else self.Data.view[idx, 1:3]
andrej@1730: 
surkovsv93@1962:             if self.VariableType in ["TIME", "TOD", "DT", "DATE"]:
surkovsv93@1962:                 value = timedelta(seconds=value)
surkovsv93@1962: 
Laurent@1193:             # Get raw value if asked
Laurent@1193:             if not raw:
Laurent@1193:                 value = TYPE_TRANSLATOR.get(
andrej@1878:                     self.VariableType, str)(value)
andrej@1730: 
Laurent@1193:             return value, forced
andrej@1730: 
Laurent@1193:         # Return raw value if asked
Laurent@1193:         if not raw and self.VariableType in ["STRING", "WSTRING"]:
Laurent@1193:             return TYPE_TRANSLATOR.get(
andrej@1878:                 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
andrej@1730: 
Laurent@1193:         # Extract data ticks
Edouard@2741:         ticks = self.Data.view[:, 0]
andrej@1730: 
Laurent@1193:         # Get nearest data from tick
Edouard@2742:         idx = min(np.searchsorted(ticks, tick), self.Data.count - 1)
andrej@1730: 
Laurent@1193:         # Adjust data index according to constraint
andrej@1766:         if adjust < 0 and ticks[idx] > tick and idx > 0 or \
andrej@1766:            adjust > 0 and ticks[idx] < tick and idx < len(ticks):
Laurent@1193:             idx += adjust
andrej@1730: 
Laurent@1193:         return idx