pyP2Monitor  1.0.0
Monitor a P2 furnace activity reading data on serial port
 All Data Structures Namespaces Functions Variables Groups Pages
p2msg.py
1 # -*- coding: utf-8 -*-#
2 
3 # Copyright 2013, 2014 Weber Yann, Weber Laurent
4 #
5 # This file is part of pyP2Monitor.
6 #
7 # pyP2Monitor is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # pyP2Monitor is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with pyP2Monitor. If not, see <http://www.gnu.org/licenses/>.
19 #
20 
21 ##@package p2msg Used to handle P2 messages
22 
23 import logging
24 import utils
25 
26 ##Use to log
27 #@see utils.getLogger()
28 logger = utils.getLogger()
29 
30 
31 ##The class managing message from the furnace
32 # @ingroup msgprocess
33 class P2Msg:
34 
35  ##@defgroup dataFormat Data format
36  # @ingroup msgprocess
37 
38  ##Huge integer format
39  #@ingroup dataFormat
40  FMT_INT = 1
41  ##Raw string format
42  #@ingroup dataFormat
43  FMT_RAW_STR = 2
44  ##Hexadecimal representation string
45  #@ingroup dataFormat
46  FMT_HEX_STR = 3
47  ##Integer list
48  #@ingroup dataFormat
49  FMT_LIST = 4
50 
51  ##Instanciate a new P2Msg object
52  #
53  # @param header The message header (aka command identifier)
54  # @param datas The message data
55  def __init__(self, header = None, datas = None):
56  #At initialisation we only store raw int list
57 
58  ##The message header or command identifier
59  self.header = None
60  ##The message data
61  self.datas = None
62 
63  if not header == None:
64  self.setHeader(header)
65  if not datas == None:
66  self.setData(datas)
67 
68  ##The data length in bytes
69  self.dataSz = None
70  ##The message checksum
71  self.checksum = None
72  ##A flag telling wether the message is in a valid state or not
73  self.valid = True
74  pass
75 
76  ##Cast an ascii string into a list of bytes (type cast only)
77  #
78  #@param strArg An ascii string
79  #@return An array of integers
80  @staticmethod
81  def str2list(strArg):
82  res = []
83  for i in range(len(strArg)):
84  res.append(ord(strArg[i]))
85  return res
86 
87  ##Convert a string with 2 digits hexadecimal numbers into a list of integers
88  #
89  # Input string example : "07474E5504"
90  #
91  #@param strArg A string with 2 hexadecimal digits integer representation (without space)
92  #@return An array of integers
93  @staticmethod
94  def hex2list(strArg):
95  res = []
96  while len(strArg) > 0:
97  res.append( int(strArg[0:2],16) )
98  strArg = strArg[2:]
99  return res;
100 
101  ##Set the P2msg header
102  #
103  #@param header The frame header (as string or integer array)
104  #
105  #@return P2Msg::valid
106  #
107  #@exception TypeError On invalid type for header param
108  def setHeader(self, header):
109  if type(header) is str:
110  self.header = P2Msg.str2list(header)
111  elif type(header) is list:
112  self.header = header
113  else:
114  raise TypeError("Waiting str or list but got "+str(type(header)))
115 
116  if len(header) != 2:
117  self.valid = False
118 
119  return self.valid
120 
121  ##Set the data size
122  # if sizeChk is True and data already set, check for validity
123  #
124  # @param recvBuff Is either a raw string from the furnace begining with the data size (header removed) or an integer representing the data size
125  # @param sizeChk A boolean telling if we have to check or not the data size (in the case where recvBuff is a raw string from serial port)
126  #
127  # @return P2Msg::valid
128  #
129  # @exception TypeError On invalid type for parameters
130  def setDataSz(self, recvBuff, sizeChk = True):
131  if type(recvBuff) is str:
132  self.dataSz = ord(recvBuff[0])
133  elif type(recvBuff) is int:
134  self.dataSz = recvBuff
135  else:
136  raise TypeError("Waiting str or int but got ",type(recvBuff))
137 
138  if sizeChk and self.datas != None and self.dataSz != len(self.datas):
139  raise Exception("Data size doesn't match actuel size of data in the message")
140 
141  return self.valid
142 
143 
144  ##Set the P2Msg data field
145  # If sizeChk is True and data size already set check for validity
146  #
147  # @param datas A raw string or an integer list representing datas
148  # @param sizeChk A boolean telling whether we check datas argument size or not
149  #
150  # @return P2Msg::valid
151  #
152  # @exception TypeError On invalid type for parameters
153  def setData(self,datas, sizeChk = True):
154  """ DEACTIVATE TEST !!! TO REACTIVATE LATER
155  if sizeChk and self.dataSz != len(datas):
156  self.valid = False
157  """
158 
159  if type(datas) is str:
160  self.datas = P2Msg.str2list(datas)
161  elif type(datas) is list:
162  self.datas = datas
163  else:
164  raise TypeError("Waiting str or list but got ",type(datas))
165 
166  if not sizeChk or self.dataSz == None:
167  self.dataSz = len(self.datas)
168 
169  return self.valid
170 
171  ##Set the cheksum
172  # If no arguments process the checksum from header and datas
173  # if checksum argument set and check is True set the checksum value and
174  # check its validity
175  #
176  # @param checksum Set to None for checksum processing from P2Msg::header P2Msg::dataSz and P2Msg::datas, else can be a raw sring or an integer array
177  # @param check Is a boolean telling if we check or not the validity of the checksum
178  #
179  # @return P2Msg::valid
180  #
181  # @exception TypeError On invalid type for parameters
182  def setChecksum(self, checksum = None, check = True):
183  if checksum != None:
184  if len(checksum) != 2:
185  self.valid = False
186  self.checksum = 0
187  elif type(checksum) is str or type(checksum) is list:
188  self.checksum = ord(checksum[1]) + (ord(checksum[0]) * 0x100)
189  elif type(checksum) is int:
190  self.checksum = checksum
191  else:
192  raise TypeError("Waiting int, str or list but got : ",type(checksum))
193 
194  if check:
195  ok = self.check()
196  self.valid = ok
197  if not ok:
198  logger.warn("Warning invalid checksum for : "+self.getStr())
199  else:
200  self.checksum = self.calcChecksum()
201 
202 
203  return self.valid
204 
205  ##Prepare a message to be ready to send
206  #
207  # @param header Set to None to leave header unchanged
208  # @param data Set to None to leave data unchanged
209  #
210  # @return None
211  def prepare(self, header = None, data = None):
212  if header != None: #Set header
213  self.setHeader(header)
214 
215  if data != None: #Set data
216  self.setData(data)
217 
218  #Data size update
219  self.setDataSz(len(self.datas))
220  #Checksum process and set
221  self.setChecksum()
222 
223  if self.header != None and self.datas != None:
224  self.valid = True
225 
226  pass
227 
228  ##Reset the value of the P2Msg::valid flag
229  #
230  #@param val The wanted value for the flag
231  def resetValid(self, val=True):
232  if val:
233  self.valid = True
234  else:
235  self.valid = False
236 
237  ##Return the data size
238  #
239  # @param fmt The wanted data format
240  #
241  # @return Formated data size
242  #
243  # @see dataFormat
244  #
245  # @exception TypeError On invalid type for parameters
246  def getDataSz(self,fmt = FMT_INT):
247  if fmt == P2Msg.FMT_INT:
248  if self.dataSz == None:
249  return 0
250  return self.dataSz
251  elif fmt == P2Msg.FMT_RAW_STR:
252  if self.dataSz == None:
253  return ""
254  return str(bytearray([self.dataSz]))
255  elif fmt == P2Msg.FMT_HEX_STR:
256  if self.dataSz == None:
257  return ""
258  return "%02X" % self.dataSz
259  else:
260  raise TypeError("Unknow or invalid format")
261 
262  ##Return the checksum
263  #
264  # @param fmt The wanted data format
265  #
266  # @return Formated checksum
267  #
268  # @see dataFormat
269  #
270  # @exception TypeError On invalid type for parameters
271  def getChecksum(self, fmt = FMT_INT):
272  if fmt == P2Msg.FMT_INT:
273  return self.checksum
274  elif fmt == P2Msg.FMT_RAW_STR:
275  return str(bytearray([self.checksum/0x100, self.checksum%0x100]))
276  elif fmt == P2Msg.FMT_HEX_STR:
277  return "%02X%02X" % (self.checksum / 0x100, self.checksum % 0x100)
278  elif fmt == P2Msg.FMT_LIST:
279  return [self.checksum/ 0x100, self.checksum % 0x100]
280  else:
281  raise TypeError("Unknow format")
282 
283  """
284  Return a list attribute in the requested format
285  """
286  ##Format an integer list
287  #
288  # @param lst The list to format
289  # @param fmt The wanted data format
290  #
291  # @return Formated list
292  #
293  # @see dataFormat
294  #
295  # @exception TypeError On invalid type for parameters
296  @staticmethod
297  def formatList(lst, fmt):
298  if fmt == P2Msg.FMT_LIST:
299  return lst
300  elif fmt == P2Msg.FMT_HEX_STR:
301  if lst == None:
302  return ""
303  res = ""
304  for i in lst:
305  res += "%02X" % i
306  return res
307  elif fmt == P2Msg.FMT_RAW_STR:
308  if lst == None:
309  return ""
310  return str(bytearray(lst))
311  else:
312  raise TypeError("Unknow or invalid format")
313 
314  ##Return the header
315  #
316  # @param fmt The wanted data format
317  #
318  # @return Formated header
319  #
320  # @see dataFormat
321  def getHeader(self, fmt = FMT_LIST):
322  return P2Msg.formatList(self.header, fmt)
323 
324  ##Return the data
325  #
326  # @param fmt The wanted data format
327  #
328  # @return Formated data
329  #
330  # @see dataFormat
331  def getData(self, fmt = FMT_LIST):
332  return P2Msg.formatList(self.datas, fmt)
333 
334 
335  ##Alias for P2Msg::getStr()
336  def __print__(self):
337  return self.getStr()
338 
339  ##Return a string representing the message in hexadecimal notation
340  #
341  #@return A string with hexadecimal number notation on two hex digits
342  def getStr(self):
343  res = ""
344  if self.header != None:
345  for h in self.header:
346  res += "%02X" % h
347  if self.datas != None:
348  res += "%02X" % len(self.datas)
349  for data in self.datas:
350  res += "%02X" % data
351  if self.checksum != None:
352  res += "%02X" % (self.checksum / 0x100)
353  res += "%02X" % (self.checksum % 0x100)
354  return res
355 
356  ##Return the raw str representing the message
357  #
358  #@return An ascii string
359  def getRaw(self):
360  res = ""
361  res += self.getHeader(P2Msg.FMT_RAW_STR)
362  res += self.getDataSz(P2Msg.FMT_RAW_STR)
363  res += self.getData(P2Msg.FMT_RAW_STR)
364  res += self.getChecksum(P2Msg.FMT_RAW_STR)
365  if self.failed():
366  logger.warn("message marked as invalid while getting raw string")
367  if not self.check():
368  logger.warn("invalid message's chekcsum while getting raw string")
369  return res
370 
371  ##Display all the informations on the message
372  def dump(self):
373  print "Header :\t"+str(self.header)
374  print "Data size =\t"+str(self.dataSz)
375  print "Datas :\t"+str(self.datas)
376  print "Checksum =\t"+"%02X%02X"%(self.checksum/0x100, self.checksum%0x100)
377  print "Message marked as",
378  if self.valid:
379  print "valid"
380  else:
381  print "not valid"
382  pass
383 
384  ##Return a string trying to display every printable ASCII character
385  #
386  # The returned string represente each data's bytes sperated with a space.
387  # Printable character are displayed as a single char when other value are displayed in 2 digits hexadecimal notation
388  #
389  # @param data The data to parse
390  # @param chrRange The character range to display
391  #
392  # @return A string
393  def dumpData(self,data,chrRange = [' ','~']):
394  res = ""
395  for c in data:
396  if c >= ord(chrRange[0]) and c <= ord(chrRange[1]):
397  res += chr(c)+" "
398  else:
399  res += "0x%02X " % c
400  return res
401 
402  ##A try to display initialisation message
403  #
404  # This functions try to display P2 furnace initialisation message in a more readable format
405  def dispInitMsg(self):
406  res = ""
407  if self.header[0] < ord('A') or self.header[0] > ord('z') or self.header[1]<ord('A') or self.header[1]>ord('z'):
408  res = "[0x%02X 0x%02X]" % (self.header[0],self.header[1])
409  else:
410  res = "[%c%c] " % (self.header[0],self.header[1])
411 
412  if self.header[0] == ord('M'):
413  hid = self.header[1]
414  if hid == ord('A') or hid == ord('B') or hid == ord('M'):
415  for d in self.datas[:5]:
416  res+="0x%02X "%d
417  res += self.dumpData(self.datas[5:])
418  elif hid == ord('D'):
419  res += chr(self.datas[0])+" "
420  for d in self.datas[1:7]:
421  res+="0x%02X "%d
422  res += self.dumpData(self.datas[7:])
423  elif hid == ord('T'):
424  res+="0x%02X "%self.datas[0]
425  res+= self.dumpData(self.datas[1:])
426  elif hid == ord('L'):
427  for d in self.datas[:11]:
428  res+="0x%02X "%d
429  res += self.dumpData(self.datas[11:])
430  elif hid == ord('F') or hid == ord('W'):
431  for d in self.datas[:3]:
432  res+="0x%02X "%d
433  res += self.dumpData(self.datas[3:])
434  else:
435  for d in self.datas:
436  res+="0x%02X "%d
437  else:
438  for d in self.datas:
439  res+="0x%02X "%d
440  return res
441 
442  ##Return the not valid flag
443  #
444  # @return the negation of P2Msg::valid
445  #
446  def failed(self):
447  return (not self.valid)
448 
449  ##Return the checksum as an integer for this message
450  #
451  # Process and return the current message checksum
452  #
453  # @return An integer representing the message's checksum
454  def calcChecksum(self):
455  chk = self.dataSz
456  for h in self.header:
457  chk += h
458  for data in self.datas:
459  chk += data
460  return chk;
461 
462  ##Check the message checksum
463  #
464  # Process the checksum and check if it is the same than the stored checksum
465  #
466  # @return A boolean value
467  def check(self):
468  return (self.getChecksum() == self.checksum)
469