controls/DebugVariablePanel/DebugVariableItem.py
author Sergey Surkov <surkovsv93@gmail.com>
Tue, 18 Oct 2016 17:44:08 +0300
changeset 1544 2969c2123105
parent 1365 debc97102b23
child 1571 486f94a8032c
permissions -rw-r--r--
Fix bug with two or more wires connected to one input. Now only one wire can be connected to one input, except BOOLean signals in LD and SFC. If user trying to connect wire with already connected input, wire highlight will become red.
Signed-off-by: Andrey Skvortsov <andrej.skvortzov@gmail.com>
#!/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) 2012: 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

import numpy
import binascii

from graphics.DebugDataConsumer import DebugDataConsumer, TYPE_TRANSLATOR

#-------------------------------------------------------------------------------
#                 Constant for calculate CRC for string variables
#-------------------------------------------------------------------------------

STRING_CRC_SIZE = 8
STRING_CRC_MASK = 2 ** STRING_CRC_SIZE - 1

#-------------------------------------------------------------------------------
#                          Debug Variable Item Class
#-------------------------------------------------------------------------------

"""
Class that implements an element that consumes debug values for PLC variable and
stores received values for displaying them in graphic panel or table
"""

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

    def GetNearestData(self, tick, adjust):
        """
        Return index of nearest data from tick given
        @param tick: Tick where find nearest data
        @param adjust: Constraint for data position from tick
                       -1: older than tick
                       1:  newer than tick
                       0:  doesn't matter
        @return: Index of nearest data
        """
        # Return immediately if data is empty
        if self.Data is None:
            return None
        
        # Extract data ticks
        ticks = self.Data[:, 0]
        
        # Get nearest data from tick
        idx = numpy.argmin(abs(ticks - tick))
        
        # Adjust data index according to constraint
        if (adjust < 0 and ticks[idx] > tick and idx > 0 or
            adjust > 0 and ticks[idx] < tick and idx < len(ticks)):
            idx += adjust
        
        return idx