pyP2Monitor  1.0.0
Monitor a P2 furnace activity reading data on serial port
 All Data Structures Namespaces Functions Variables Groups Pages
p2proto.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 p2proto
22 # Handle the P2 communication protocol
23 
24 import time
25 import datetime
26 import sys
27 import logging
28 
29 import p2com
30 from p2com import *
31 
32 import p2dbstore
33 from p2dbstore import *
34 
35 ##Use to log
36 #@see utils.getLogger()
37 logger = utils.getLogger()
38 
39 
40 ##Class implementing and handling the communication protocol with the furnace
41 #@ingroup comproto
42 class P2Furn:
43 
44  ##@defgroup P2ProtoStages Communication stages
45  #@ingroup comproto
46 
47  ##Before initialisation stage
48  #@ingroup P2ProtoStages
49  STAGE_PREINIT = 0
50  ##Authentication stage
51  #@ingroup P2ProtoStages
52  STAGE_LOGIN = 1
53  ##Initialisation exchange stage
54  #@ingroup P2ProtoStages
55  STAGE_INIT = 2
56  ##After initialisation stage
57  #@ingroup P2ProtoStages
58  STAGE_POST_INIT = 3
59  ##Data exchange stage
60  #@ingroup P2ProtoStages
61  STAGE_XCHG = 4
62 
63  ##@defgroup P2ProtoUsers Furnace user's value
64  #@ingroup comproto
65 
66  ##Plumber user
67  #@ingroup P2ProtoUsers
68  USER_PLUMBER = "52610300007B"
69  ##Normal user
70  #@ingroup P2ProtoUsers
71  USER_NORMAL = "526103000001"
72  ##Alternate normal user
73  #@ingroup P2ProtoUsers
74  USER_NORMAL_ALT = "5261030000F3"
75  ##Service user
76  #@ingroup P2ProtoUsers
77  USER_SERVICE = "52610300FFF9"
78 
79 
80  ##Instanciate a new P2Furn
81  #
82  # @param serial_port The serial port filename (eg : /dev/ttyS0 )
83  def __init__(self, serial_port):
84 
85  ##The serial port filename
86  self.port = serial_port
87  ##The Associated P2Com object
88  self.com = P2Com(serial_port)
89  ##Stores the current stage
90  #@see P2ProtoStages
91  self.curStage = P2Furn.STAGE_PREINIT
92 
93  pass
94 
95  ##Return user hex value used to auth, given a user name
96  #
97  #@param user the user name
98  #
99  #@return An hex string representing the user id
100  @staticmethod
101  def userId(user):
102  res = P2Furn.USER_NORMAL
103 
104  if user == 'service':
105  res = P2Furn.USER_SERVICE
106  elif user == 'normal_alt':
107  res = P2Furn.USER_NORMAL_ALT
108  elif user == 'plumber':
109  res = P2Furn.USER_PLUMBER
110 
111  return res
112 
113  ##Restart the serial port
114  def restartSerialPort(self):
115  self.com.close()
116  self.com = P2Com(0)
117  pass
118 
119  ##Read a message from the furnace
120  #
121  # Simply call P2Com::read()
122  #
123  #@return A P2Msg object
124  #
125  #@see P2Com::read()
126  def readMsg(self):
127  return self.com.read()
128 
129  ##Send a message to the furnace
130  #
131  # Simply call P2Com::sendMsg()
132  #
133  #@param msg A P2Msg object
134  #@see P2Com::sendMsg()
135  def sendMsg(self, msg):
136  self.com.sendMsg(msg)
137 
138  ##Begin the authentication stage
139  #
140  # @param user Is the hexadecimal representation of the message to send to authenticate
141  # @param maxRetry The maximum number of retry before exit and consider as failed
142  # @param retryWait The number of seconds between two authentication try
143  #
144  # @see P2ProtoUsers
145  #
146  # @exception Exception On invalid type for parameters
147  def runAuth(self, user = USER_SERVICE, maxRetry = 3, retryWait = 10):
148 
149  logger.info("Entering authentication stage")
150  logger.debug("Trying to auth with '"+user+"'")
151 
152  retry = True
153  retryCnt = 0
154 
155  self.curStage = P2Furn.STAGE_LOGIN
156  """
157  Sending user Id
158  """
159  retry = True
160  while retry:
161  logger.debug("Sending user Id, waiting for reply.")
162  self.com.write(user)
163 
164  msg = self.com.read()
165  retry = False
166  logger.debug("Answer received : "+msg.getStr())
167  pass
168 
169  ##Run the initialisation stage
170  #
171  # @exception P2ComError On protocol error
172  def runInit(self):
173 
174  logger.info("Entering initialisation stage")
175 
176  """
177  Waiting for the initialisation to end
178  """
179  inMsg = None
180  outMsg = P2Msg()
181  #First headers value
182  firstHeader = [0x4D,0x41]
183  outData = [0x01]
184  outMsg.prepare(firstHeader, outData)
185 
186  self.curStage = P2Furn.STAGE_INIT
187 
188  counter = 1
189  """
190  Sending message with same header as reply while reply's header doesn't
191  begin with "4D3"
192  """
193  while self.curStage == P2Furn.STAGE_INIT:
194  logger.debug("Init message exchange #"+str(counter))
195  self.com.sendMsg(outMsg)
196 
197  try:
198  inMsg = self.com.read()
199  except P2ComError as e:
200  if e.getErrno() == 9:
201  raise e
202 
203  if e.getData() == None:
204  logger.error("Message failure : no message")
205  else:
206  logger.error("Message failure : "+e.getData().getStr())
207 
208  ##########################
209  #
210  # WARNING HERE WE SET OUR
211  # REPLY HEADER FROM AN
212  # INVALID/INCOMPLETE
213  # MESSAGE
214  #
215  ###########################
216  recvHeader = e.getData().getHeader(P2Msg.FMT_LIST)
217 
218  logger.debug("Received headers : "+str(recvHeader))
219  else :
220  #ici on pourrait peut etre check que le premier octet de header est bien 0x4D
221  recvHeader = inMsg.getHeader(P2Msg.FMT_LIST)
222 
223  counter += 1
224  #Exit if header begin with "4d3"
225  if recvHeader[0] == 0x4D and (recvHeader[1] & 0xF0) == 0x30:
226  self.curStage = P2Furn.STAGE_POST_INIT
227  else:
228  outMsg.prepare(recvHeader)
229 
230  logger.info("Initialisation successfully terminated with "+str(counter)+" exchange between computer and furnace")
231 
232  """
233  First initialisaion stage end
234  """
235  #Read 32 times M2 values
236  outMsg.prepare([0x4D,0x32],[0x01])
237  nbMaxTs = 33
238  ser = self.com.getCom()
239  for i in range(nbMaxTs):
240  self.com.sendMsg(outMsg)
241  time.sleep(0.15) #Important sleep !
242  if ser.inWaiting() > 0:
243  inMsg = self.com.read()
244  logger.info("2nd init message received")
245  logger.debug("Message : "+inMsg.getStr())
246  else:
247  logger.debug("No message...")
248 
249  logger.debug(str(nbMaxTs-i)+" message left before 2nd init end.")
250 
251  #Send rb request
252  logger.info("Sending rb request")
253  outMsg.prepare([0x52,0x62],[0x00,0x00,0x01])
254  try:
255  self.com.sendMsg(outMsg)
256  #Read ack
257  inMsg = self.com.read() #usually "0x52 0x62 0x01 0x01 0x00 0xb6"
258  logger.info("Rb acknowledge received")
259  except P2ComError as e:
260  logger.error("Timeout waiting rb aknowledge")
261 
262 
263  logger.info("Second Init stage success.")
264 
265  #send M2 request
266  logger.info("Waiting 3s before sending the first M2 request")
267  time.sleep(3)
268  logger.debug("Sending the first M2 request")
269  outMsg.prepare([0x4D,0x32],[0x01])
270 
271  retryMax = 20
272  i=0
273  while i<retryMax:
274  try:
275  self.com.sendMsg(outMsg)
276  #Read ack
277  inMsg = self.com.read()
278  logger.debug("M2 Acknowledge received")
279  break
280  except P2ComError as e:
281  i+=1
282  logger.warning(str(i)+" timeout waiting acknowledge for 3th init stage M2 message, retrying...")
283 
284  if i == retryMax:
285  logger.error("Abording... Too much timeout received for 3th init stage M2 message.")
286 
287  #Change state
288  self.curStage = P2Furn.STAGE_XCHG
289 
290  logger.info("Initialisation successfully terminated...")
291  pass
292 
293  ##Run the data retrieve function
294  #
295  # @param storage is a dict storing each data storage. Possible keys values are "sqlite" for sqlite db (value is the db file) or "file" for ascii dump (value is the dump file)
296  # @param dateFromP2 is True when we want the furnace date and time, if false we take the date and time from the computer
297  #
298  # @exception TypeError On invalid type for parameter storage
299  def readData(self, waitdata = 0.5, storage=[("sqlite", "p2.db")], dateFromP2 = False):
300 
301  logger.info("Entering data exchange mode")
302 
303  #Local variable initialisation
304  inMsg = P2Msg()
305  outMsg = P2Msg()
306 
307  headers = {"m1" : [0x4D,0x31], "m2" : [0x4D,0x32], "m3" : [0x4D,0x33]}
308 
309  #m1sending = True
310  m1sending = False
311 
312  #Storage initialisation
313  storObj = []
314  for (method,name) in storage:
315  if method == "sqlite":
316  storObj.append(("sqlite", P2DbStore(name)))
317  elif method == "lastdata":
318  storObj.append(("lastdata", name))
319  elif method == "file":
320  storObj.append(("file", open(name, 'a')))
321  elif method == "csv":
322  storObj.append(('csv',csv.writer(open(name, 'wb'), delimiter=',')))
323  else:
324  raise TypeError('Waiting for a tuple of the form (["file" | "sqlite" | "csv", filename), but got ('+str(method)+','+str(name)+')')
325 
326  outMsg.prepare(headers["m2"], [0x01])
327  #Fake m2 receive
328  inMsg.prepare([0x4D,0x32],[0x01])
329 
330  curDate = None
331 
332  #infinite loop...
333  while True:
334  #Test wich header we have
335  inHead = inMsg.getHeader(P2Msg.FMT_HEX_STR)
336  if inHead == "4D33":
337  #We received a M3
338  outMsg.prepare(headers["m3"])
339  logger.info("M3 message received")
340  else:
341  if m1sending:
342  logger.debug("Sending a M1 message")
343  outMsg.prepare(headers["m1"])
344  m1sending = False
345  else:
346  logger.debug("Sending a M2 message")
347  outMsg.prepare(headers["m2"])
348  m1sending = True
349  #Send the prepared message
350  self.com.sendMsg(outMsg)
351 
352  #And read the reply
353  inMsg = self.com.read()
354 
355  """
356  Incoming frame process
357  """
358  if inMsg.getHeader(P2Msg.FMT_HEX_STR) == "4D31" and (curDate != None or not dateFromP2):
359 
360  logger.debug("M1 received")
361  #If we dont want the date from the P2 we take the computer's date and time
362  if not dateFromP2:
363  curDate = datetime.datetime.now()
364 
365  #Store data on each selected storage
366  for (family, obj) in storObj:
367  if family == "sqlite":
368  #Only store frame with valid checksum
369  if inMsg.check():
370  obj.insert(curDate.strftime("%s"), inMsg.getData(P2Msg.FMT_HEX_STR))
371  elif family == "lastdata":
372  if inMsg.check():
373  lfile = open(obj, "w+")
374  lfile.write(curDate.strftime("%s")+"\n"+inMsg.getData(P2Msg.FMT_HEX_STR))
375  lfile.close()
376  elif family == "csv":
377  if inMsg.check():
378  obj.writerow([curDate.strftime("%s")]+inMsg.getData(P2Msg.FMT_LIST))
379  elif family == "file":
380  if inMsg.check():
381  obj.write(str(curDate.strftime("%s"))+" ::"+inMsg.getData(P2Msg.FMT_HEX_STR))
382  else:
383  obj.write(str(curDate.strftime("%s"))+" :invalid:"+inMsg.getData(P2Msg.FMT_HEX_STR))
384 
385  curTimestamp = None
386  elif dateFromP2:
387  logger.debug("M2 received")
388  #If we want date and time from the furnace take it...
389  if inMsg.getHeader(P2Msg.FMT_HEX_STR) == "4D32":
390  #we got a date
391  msgData = inMsg.getData(P2Msg.FMT_LIST)
392  #msgData[2] is day of week. !!! Warning : Y3K problem ;o) !!!
393  curDate = datetime.datetime(msgData[0]+2000,msgData[2],msgData[3],msgData[4],msgData[5],msgData[6])
394 
395  logger.debug("Received message : "+inMsg.getStr())
396  #sleeping as asked
397  time.sleep(waitdata)
398  pass
399 
400  ##Close the serial port and reset stage lag
401  def stop(self):
402  self.curStage = P2Furn.STAGE_PREINIT
403  self.com.close()
404  pass
405