|
1 #!/usr/bin/env python |
|
2 # -*- coding: utf-8 -*- |
|
3 |
|
4 #This file is part of PLCOpenEditor, a library implementing an IEC 61131-3 editor |
|
5 #based on the plcopen standard. |
|
6 # |
|
7 #Copyright (C) 2012: Edouard TISSERANT and Laurent BESSARD |
|
8 # |
|
9 #See COPYING file for copyrights details. |
|
10 # |
|
11 #This library is free software; you can redistribute it and/or |
|
12 #modify it under the terms of the GNU General Public |
|
13 #License as published by the Free Software Foundation; either |
|
14 #version 2.1 of the License, or (at your option) any later version. |
|
15 # |
|
16 #This library is distributed in the hope that it will be useful, |
|
17 #but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
18 #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
|
19 #General Public License for more details. |
|
20 # |
|
21 #You should have received a copy of the GNU General Public |
|
22 #License along with this library; if not, write to the Free Software |
|
23 #Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
|
24 |
|
25 import numpy |
|
26 import binascii |
|
27 |
|
28 from graphics.DebugDataConsumer import DebugDataConsumer, TYPE_TRANSLATOR |
|
29 |
|
30 #------------------------------------------------------------------------------- |
|
31 # Constant for calculate CRC for string variables |
|
32 #------------------------------------------------------------------------------- |
|
33 |
|
34 STRING_CRC_SIZE = 8 |
|
35 STRING_CRC_MASK = 2 ** STRING_CRC_SIZE - 1 |
|
36 |
|
37 #------------------------------------------------------------------------------- |
|
38 # Debug Variable Item Class |
|
39 #------------------------------------------------------------------------------- |
|
40 |
|
41 """ |
|
42 Class that implements an element that consumes debug values for PLC variable and |
|
43 stores received values for displaying them in graphic panel or table |
|
44 """ |
|
45 |
|
46 class DebugVariableItem(DebugDataConsumer): |
|
47 |
|
48 def __init__(self, parent, variable, store_data=False): |
|
49 """ |
|
50 Constructor |
|
51 @param parent: Reference to debug variable panel |
|
52 @param variable: Path of variable to debug |
|
53 """ |
|
54 DebugDataConsumer.__init__(self) |
|
55 |
|
56 self.Parent = parent |
|
57 self.Variable = variable |
|
58 self.StoreData = store_data |
|
59 |
|
60 # Get Variable data type |
|
61 self.RefreshVariableType() |
|
62 |
|
63 def __del__(self): |
|
64 """ |
|
65 Destructor |
|
66 """ |
|
67 # Reset reference to debug variable panel |
|
68 self.Parent = None |
|
69 |
|
70 def SetVariable(self, variable): |
|
71 """ |
|
72 Set path of variable |
|
73 @param variable: Path of variable to debug |
|
74 """ |
|
75 if self.Parent is not None and self.Variable != variable: |
|
76 # Store variable path |
|
77 self.Variable = variable |
|
78 # Get Variable data type |
|
79 self.RefreshVariableType() |
|
80 |
|
81 # Refresh debug variable panel |
|
82 self.Parent.RefreshView() |
|
83 |
|
84 def GetVariable(self, mask=None): |
|
85 """ |
|
86 Return path of variable |
|
87 @param mask: Mask to apply to variable path [var_name, '*',...] |
|
88 @return: String containing masked variable path |
|
89 """ |
|
90 # Apply mask to variable name |
|
91 if mask is not None: |
|
92 # '#' correspond to parts that are different between all items |
|
93 |
|
94 # Extract variable path parts |
|
95 parts = self.Variable.split('.') |
|
96 # Adjust mask size to size of variable path |
|
97 mask = mask + ['*'] * max(0, len(parts) - len(mask)) |
|
98 |
|
99 # Store previous mask |
|
100 last = None |
|
101 # Init masked variable path |
|
102 variable = "" |
|
103 |
|
104 for m, p in zip(mask, parts): |
|
105 # Part is not masked, add part prefixed with '.' is previous |
|
106 # wasn't masked |
|
107 if m == '*': |
|
108 variable += ('.' if last == '*' else '') + p |
|
109 |
|
110 # Part is mask, add '..' if first or previous wasn't masked |
|
111 elif last is None or last == '*': |
|
112 variable += '..' |
|
113 |
|
114 last = m |
|
115 |
|
116 return variable |
|
117 |
|
118 return self.Variable |
|
119 |
|
120 def RefreshVariableType(self): |
|
121 """ |
|
122 Get and store variable data type |
|
123 """ |
|
124 self.VariableType = self.Parent.GetDataType(self.Variable) |
|
125 # Reset data stored |
|
126 self.ResetData() |
|
127 |
|
128 def GetVariableType(self): |
|
129 """ |
|
130 Return variable data type |
|
131 @return: Variable data type |
|
132 """ |
|
133 return self.VariableType |
|
134 |
|
135 def GetData(self, start_tick=None, end_tick=None): |
|
136 """ |
|
137 Return data stored contained in given range |
|
138 @param start_tick: Start tick of given range (default None, first data) |
|
139 @param end_tick: end tick of given range (default None, last data) |
|
140 @return: Data as numpy.array([(tick, value, forced),...]) |
|
141 """ |
|
142 # Return immediately if data empty or none |
|
143 if self.Data is None or len(self.Data) == 0: |
|
144 return self.Data |
|
145 |
|
146 # Find nearest data outside given range indexes |
|
147 start_idx = (self.GetNearestData(start_tick, -1) |
|
148 if start_tick is not None |
|
149 else 0) |
|
150 end_idx = (self.GetNearestData(end_tick, 1) |
|
151 if end_tick is not None |
|
152 else len(self.Data)) |
|
153 |
|
154 # Return data between indexes |
|
155 return self.Data[start_idx:end_idx] |
|
156 |
|
157 def GetRawValue(self, index): |
|
158 """ |
|
159 Return raw value at given index for string variables |
|
160 @param index: Variable value index |
|
161 @return: Variable data type |
|
162 """ |
|
163 if (self.VariableType in ["STRING", "WSTRING"] and |
|
164 index < len(self.RawData)): |
|
165 return self.RawData[idx][0] |
|
166 return "" |
|
167 |
|
168 def GetValueRange(self): |
|
169 """ |
|
170 Return variable value range |
|
171 @return: (minimum_value, maximum_value) |
|
172 """ |
|
173 return self.MinValue, self.MaxValue |
|
174 |
|
175 def ResetData(self): |
|
176 """ |
|
177 Reset data stored when store data option enabled |
|
178 """ |
|
179 if self.StoreData and self.IsNumVariable(): |
|
180 # Init table storing data |
|
181 self.Data = numpy.array([]).reshape(0, 3) |
|
182 |
|
183 # Init table storing raw data if variable is strin |
|
184 self.RawData = ([] |
|
185 if self.VariableType in ["STRING", "WSTRING"] |
|
186 else None) |
|
187 |
|
188 # Init Value range variables |
|
189 self.MinValue = None |
|
190 self.MaxValue = None |
|
191 |
|
192 else: |
|
193 self.Data = None |
|
194 |
|
195 # Init variable value |
|
196 self.Value = "" |
|
197 |
|
198 def IsNumVariable(self): |
|
199 """ |
|
200 Return if variable data type is numeric. String variables are |
|
201 considered as numeric (string CRC) |
|
202 @return: True if data type is numeric |
|
203 """ |
|
204 return (self.Parent.IsNumType(self.VariableType) or |
|
205 self.VariableType in ["STRING", "WSTRING"]) |
|
206 |
|
207 def NewValue(self, tick, value, forced=False): |
|
208 """ |
|
209 Function called by debug thread when a new debug value is available |
|
210 @param tick: PLC tick when value was captured |
|
211 @param value: Value captured |
|
212 @param forced: Forced flag, True if value is forced (default: False) |
|
213 """ |
|
214 DebugDataConsumer.NewValue(self, tick, value, forced) |
|
215 |
|
216 if self.Data is not None: |
|
217 # String data value is CRC |
|
218 num_value = (binascii.crc32(value) & STRING_CRC_MASK |
|
219 if self.VariableType in ["STRING", "WSTRING"] |
|
220 else float(value)) |
|
221 |
|
222 # Update variable range values |
|
223 self.MinValue = (min(self.MinValue, num_value) |
|
224 if self.MinValue is not None |
|
225 else num_value) |
|
226 self.MaxValue = (max(self.MaxValue, num_value) |
|
227 if self.MaxValue is not None |
|
228 else num_value) |
|
229 |
|
230 # Translate forced flag to float for storing in Data table |
|
231 forced_value = float(forced) |
|
232 |
|
233 # In the case of string variables, we store raw string value and |
|
234 # forced flag in raw data table. Only changes in this two values |
|
235 # are stored. Index to the corresponding raw value is stored in |
|
236 # data third column |
|
237 if self.VariableType in ["STRING", "WSTRING"]: |
|
238 raw_data = (value, forced_value) |
|
239 if len(self.RawData) == 0 or self.RawData[-1] != raw_data: |
|
240 extra_value = len(self.RawData) |
|
241 self.RawData.append(raw_data) |
|
242 else: |
|
243 extra_value = len(self.RawData) - 1 |
|
244 |
|
245 # In other case, data third column is forced flag |
|
246 else: |
|
247 extra_value = forced_value |
|
248 |
|
249 # Add New data to stored data table |
|
250 self.Data = numpy.append(self.Data, |
|
251 [[float(tick), num_value, extra_value]], axis=0) |
|
252 |
|
253 # Signal to debug variable panel to refresh |
|
254 self.Parent.HasNewData = True |
|
255 |
|
256 def SetForced(self, forced): |
|
257 """ |
|
258 Update Forced flag |
|
259 @param forced: New forced flag |
|
260 """ |
|
261 # Store forced flag |
|
262 if self.Forced != forced: |
|
263 self.Forced = forced |
|
264 |
|
265 # Signal to debug variable panel to refresh |
|
266 self.Parent.HasNewData = True |
|
267 |
|
268 def SetValue(self, value): |
|
269 """ |
|
270 Update value. |
|
271 @param value: New value |
|
272 """ |
|
273 # Remove quote and double quote surrounding string value to get raw value |
|
274 if (self.VariableType == "STRING" and |
|
275 value.startswith("'") and value.endswith("'") or |
|
276 self.VariableType == "WSTRING" and |
|
277 value.startswith('"') and value.endswith('"')): |
|
278 value = value[1:-1] |
|
279 |
|
280 # Store variable value |
|
281 if self.Value != value: |
|
282 self.Value = value |
|
283 |
|
284 # Signal to debug variable panel to refresh |
|
285 self.Parent.HasNewData = True |
|
286 |
|
287 def GetValue(self, tick=None, raw=False): |
|
288 """ |
|
289 Return current value or value and forced flag for tick given |
|
290 @return: Current value or value and forced flag |
|
291 """ |
|
292 # If tick given and stored data option enabled |
|
293 if tick is not None and self.Data is not None: |
|
294 |
|
295 # Return current value and forced flag if data empty |
|
296 if len(self.Data) == 0: |
|
297 return self.Value, self.IsForced() |
|
298 |
|
299 # Get index of nearest data from tick given |
|
300 idx = self.GetNearestData(tick, 0) |
|
301 |
|
302 # Get value and forced flag at given index |
|
303 value, forced = self.RawData[int(self.Data[idx, 2])] \ |
|
304 if self.VariableType in ["STRING", "WSTRING"] \ |
|
305 else self.Data[idx, 1:3] |
|
306 |
|
307 # Get raw value if asked |
|
308 if not raw: |
|
309 value = TYPE_TRANSLATOR.get( |
|
310 self.VariableType, str)(value) |
|
311 |
|
312 return value, forced |
|
313 |
|
314 # Return raw value if asked |
|
315 if not raw and self.VariableType in ["STRING", "WSTRING"]: |
|
316 return TYPE_TRANSLATOR.get( |
|
317 self.VariableType, str)(self.Value) |
|
318 return self.Value |
|
319 |
|
320 def GetNearestData(self, tick, adjust): |
|
321 """ |
|
322 Return index of nearest data from tick given |
|
323 @param tick: Tick where find nearest data |
|
324 @param adjust: Constraint for data position from tick |
|
325 -1: older than tick |
|
326 1: newer than tick |
|
327 0: doesn't matter |
|
328 @return: Index of nearest data |
|
329 """ |
|
330 # Return immediately if data is empty |
|
331 if self.Data is None: |
|
332 return None |
|
333 |
|
334 # Extract data ticks |
|
335 ticks = self.Data[:, 0] |
|
336 |
|
337 # Get nearest data from tick |
|
338 idx = numpy.argmin(abs(ticks - tick)) |
|
339 |
|
340 # Adjust data index according to constraint |
|
341 if (adjust < 0 and ticks[idx] > tick and idx > 0 or |
|
342 adjust > 0 and ticks[idx] < tick and idx < len(ticks)): |
|
343 idx += adjust |
|
344 |
|
345 return idx |