Project

General

Profile

Statistics
| Revision:

root / trunk / swipe / tooltron.py @ 265

History | View | Annotate | Download (16.8 KB)

1
#!/usr/bin/python
2

    
3
"""
4
  This file is part of Tooltron.
5
 
6
  Tooltron is free software: you can redistribute it and/or modify
7
  it under the terms of the Lesser GNU General Public License as published by
8
  the Free Software Foundation, either version 3 of the License, or
9
  (at your option) any later version.
10
 
11
  Tooltron is distributed in the hope that it will be useful,
12
  but WITHOUT ANY WARRANTY; without even the implied warranty of
13
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
  Lesser GNU General Public License for more details.
15
  You should have received a copy of the Lesser GNU General Public License
16
  along with Tooltron.  If not, see <http://www.gnu.org/licenses/>.
17

18
  Copyright 2009 Bradford Neuman <bneuman@andrew.cmu.edu>
19

20
"""
21

    
22
################################################################################
23
## tooltron.py is the main tooltron server
24
# It connects to the civicrm database of mysql and sends commands to
25
# the cardbox and tools.  It must be connected over usb to the card
26
# reader, and the two arguments specify the devices to use for the
27
# serial communication to the card and tool boxes.
28
#
29
# This script requires the modules seen below, which may not be
30
# present by default
31
#
32
# Because of a problem with card scans buffering in stdio, there is a
33
# seperate thread which stores the last swiped id. To cleanly exit the
34
# program type C-d
35
#
36
# email bradneuman@gmail.com if you have any questions
37
################################################################################
38

    
39

    
40
import re
41
import sys
42
import MySQLdb
43
import getpass
44
import datetime
45
from datetime import date
46
from time import *
47
sys.path.append('../common')
48
import common
49

    
50
keypadTimeout = 12 #in seconds
51

    
52
#this table maps which keypad button to press for each tool. This
53
#list should probably be printed next to the swipe box
54
tools = {
55
   'Bandsaw':[],
56
   'DrillPress':['3','8'],
57
   'Mill':['4'],
58
   'Lathe':[],
59
   #HACK: since the saw isn't used I use it for testing random boards
60
   'ChopMiterSaw':['1','2','5','6','7','9']
61
}
62

    
63
#this table maps keypad button numbers to tool IDs. The default is
64
#that the tool id is 10+the keypad number, but this would change if
65
#boards were swapped out. The tool ID number should be printed on the
66
#tool board, but needs to be hand coded into the toolbox code
67
ids = {
68
   '1':11,
69
   '2':12,
70
   '3':13,
71
   '4':14,
72
   '5':15,
73
   '6':16,
74
   '7':17,
75
   '8':18,
76
   '9':19,
77
   '0':13
78
}
79

    
80
#################################################
81
#this code spawns a thread which always puts the last card id in the
82
#lastid variable.
83
#################################################
84
import threading
85
import time
86
import select
87
import os
88

    
89
lastid = None
90
idready = False
91
threadDone = False
92
threadDoneExcp = None
93

    
94
cv = threading.Condition()
95

    
96
class ctrlDException (Exception):
97
   def __init__(self,str):
98
      Exception.__init__(self)
99
      self.str = str
100

    
101
   def __str__(self):
102
      return "Ctrl-D pressed: " + self.str
103

    
104
class idThread ( threading.Thread ):
105

    
106
   def __init__ (self):
107
      threading.Thread.__init__(self)
108
      self.wpipe = None
109

    
110
   def stop (self):
111
      self.stop_thread = True
112
      if self.wpipe != None:
113
         # write to this pipe so that the select will drop out
114
         self.wpipe.write('!')
115

    
116
   def run (self):
117
       global lastid
118
       global idready
119
       global threadDone
120
       global threadDoneExcp
121
       self.stop_thread = False
122

    
123
       [r,w] = os.pipe()
124
       self.rpipe = os.fdopen(r,'r')
125
       self.wpipe = os.fdopen(w,'w')
126

    
127
       try:
128
          while not self.stop_thread:
129
             # hang until we either get keyboard input or are told to stop
130
             [active,a,b] = select.select([sys.stdin,self.rpipe],[],[])
131

    
132
             if not self.stop_thread:
133
                l = raw_input()
134

    
135
                cv.acquire()
136
                try:
137
                   id = re.search('%([0-9]*)=.*', l)
138
                   if id != None:
139
                      lastid = id
140
                      print "!!! setting idready"
141
                      idready = True
142
                      cv.notify()
143
                   else:
144
                      ifready = False
145
                finally:
146
                   cv.release()
147
          print "thread stopped"
148

    
149
       except EOFError:
150
           cv.acquire()
151
           idready = True
152
           lastid=None
153
           threadDoneExcp=ctrlDException("thread exiting cleanly")
154
           cv.notify()
155
           cv.release()
156
       except Exception, e:
157
          threadDoneExcp = e #TODO: provide a line number or something?
158
       finally:
159
          print "id thread getting ready to exit..."
160
          threadDone = True
161
          cv.acquire()
162
          idready = False
163
          cv.notify()
164
          cv.release()
165
          print "id thread exiting"
166
          
167
#end thread
168
###############################################
169

    
170
try:
171
   flog = open("/var/log/tooltron", "a")
172
except IOError as e:
173
   print "\n*********************************************"
174
   print "File /var/log/tooltron could not be opened!"
175
   print e
176
   print "Using local file 'tooltron.log'"
177
   print "*********************************************\n"
178
   flog = open("tooltron.log","a")
179
finally:
180
   print flog
181

    
182
def logMessage(str):
183
   flog.write(strftime("%Y.%m.%d %H:%M:%S ") + str + "\n")
184
   flog.flush()
185

    
186
logMessage("----- SERVER STARTED -----")
187

    
188
def keypad2toolID(t):
189
   if t in ids:
190
      return ids[t]
191
   else:
192
      return -1
193

    
194
def reverseTool(tn):
195
   for t in tools:
196
      if tn in tools[t]:
197
         return t
198

    
199
   return 'invalid'
200

    
201
#keep count of successive non-acks
202
toolFails = tools.copy()
203
for t in toolFails:
204
   toolFails[t] = 0
205

    
206
def get_last_id():
207
    global idready
208
    global lastid
209
    global threadDoneExcp
210

    
211
    got = None
212

    
213
    print ">>> locking for last_id"
214

    
215
    cv.acquire()
216
    while not idready:
217
       if threadDoneExcp != None:
218
          print "thread has been exited"
219
          raise threadDoneExcp
220

    
221
       print ">>> wait"
222
       try:
223
          cv.wait()
224
       # if the user hits ctrl-c, cleanly exit
225
       except KeyboardInterrupt:
226
          print "cv.wait() got a keyboard interupt!"
227
          cv.release()
228
          return None
229

    
230
    print ">>> idready set"
231

    
232
    got = lastid
233
    idready = False
234
    cv.release()
235
    print ">>> done"
236

    
237
    return got
238

    
239
def clear_id():
240
    global idready
241
    global lastid
242

    
243
    print "=== locking to clear idready flag"
244
    cv.acquire()
245
    idready = False
246
    lastid = None
247
    cv.release()
248
    print "=== done"
249

    
250
    return
251

    
252
if len(sys.argv) < 2:
253
   print "usage: tooltron.py /path/to/bus/device"
254
   exit(-1)
255

    
256
if not common.initBus(sys.argv[1]):
257
    print "usage: tooltron.py /path/to/bus/device"
258
else:
259

    
260
   pw = getpass.getpass("mysql password: ")
261

    
262
   #start the id scan thread AFTER the getpass prompt
263
   idt = idThread()
264
   idt.start()
265
   
266
   # If there are any uncaught exceptions in the rest of the code, we need to kill the id thread
267
   # so the program as a whole will exit
268
   try:
269
      cursor = None
270
      qry = "SELECT tools_6 FROM civicrm_value_roboclub_info_2 WHERE card_number_1 = "
271

    
272
      def sendTool(t):
273
         tn = keypad2toolID(t)
274
         common.sendTool(tn)
275

    
276
      # This function eats the rest of the packet where data is what we have so far
277
      def flushPacket(data):
278
         while len(data) < 6:
279
            data += bus.read(1)
280

    
281
         plen = data[4]
282
         if plen > 0:
283
            bus.read(ord(plen))
284

    
285
      while True:
286
         id = get_last_id()
287

    
288
         if id != None:
289
            print "\--------------------\n",strftime("%Y.%m.%d %H:%M:%S \n"),"id# ", id.group(1)
290

    
291

    
292
            # Send a key request
293
            common.sendMessage(2, common.TT_GET_KEY, "")
294
            
295
            # listen for an ACK or a key response
296
            msg = common.readMessage()
297
            if msg == None:
298
               print "ERROR: Timeout waiting for response from key request!"
299
               continue
300

    
301
            # resp hols the key response, so set it to None since we don't have one yet
302
            resp = None
303

    
304
            [src,dest,cmd,data] = msg
305
            src = ord(src)
306
            dest = ord(dest)
307
            # print "tooltron.py has message:"
308
            # print "src:", src
309
            # print "dest:", dest
310
            # print "cmd:", cmd
311
            # print "data: {",data,"}"
312

    
313
            #TODO: no magic number '2' for cardbox or '1' for server
314
            if src == 2 and dest == 1:
315
               if cmd == common.TT_ACK:
316
                  print "got ACK from cardbox"
317
               elif cmd == common.TT_SEND_KEY:
318
                  print "got key response instead of ACK from cardbox"
319
                  print "using key: ", data
320
                  resp = data[0]
321
               else:
322
                  print "ERROR: got some other packet type: ", cmd, " (data = {", data, "})"
323
                  continue
324
            else:
325
               print "ERROR: got confusing packet. Excpecting something from cardbox (2) but got:"
326
               common.printMsg(msg)
327
               continue
328

    
329
            # start the timer so we can timeout if we are still waiting for a key response
330
            startTime = time.time()
331

    
332
            # while we are (or might be) waiting for a key response, do the MySQL stuff
333
            if cursor != None:
334
               cursor.close()
335

    
336
            db = MySQLdb.connect(host="roboclub8.frc.ri.cmu.edu", user="tooltron", passwd=pw, db="civicrm")
337
            print "connected to mysql server"
338
            cursor = db.cursor()
339
            print "got db cursor"
340

    
341
            cursor.execute(qry + id.group(1))
342

    
343
            result = cursor.fetchall()
344

    
345
            acl = []
346

    
347
            for r in result:
348
               tls = r[0].split("\x01")
349
               for t in tls:
350
                  if t != '':
351
                     try:
352
                        acl.extend (tools[t])
353
                     except KeyError:
354
                        #this doesn't really matter
355
                        pass
356

    
357
            print "user has access to:", acl
358

    
359
            user = ""
360

    
361
            #query for name
362
            qry2 = "SELECT civicrm_contact.display_name \
363
                    FROM civicrm_value_roboclub_info_2 \
364
                    INNER JOIN (civicrm_contact) \
365
                    ON civicrm_value_roboclub_info_2.entity_id = civicrm_contact.id \
366
                    WHERE civicrm_value_roboclub_info_2.card_number_1 = " + id.group(1) + ";"
367

    
368
            cursor.execute(qry2)
369
            result = cursor.fetchall()
370

    
371
            if len(result) > 0:
372
               user += result[0][0]
373

    
374
            qry2 = "SELECT civicrm_email.email \
375
                    FROM civicrm_value_roboclub_info_2 \
376
                    INNER JOIN (civicrm_email) \
377
                    ON civicrm_email.contact_id = civicrm_value_roboclub_info_2.entity_id \
378
                    WHERE civicrm_value_roboclub_info_2.card_number_1 = " + id.group(1) + ";"
379

    
380
            cursor.execute(qry2)
381
            result = cursor.fetchall()
382

    
383
            if len(result) > 0:
384
               user += " ("+result[0][0]+")"
385

    
386
            if user == "":
387
               user = str(id.group(1))
388

    
389
            print user
390

    
391

    
392
            # check if member has paid recently 
393

    
394
            # first, figure out what semester it is
395
            today = date.today()
396

    
397
            # semester start dates
398
            aug20 = datetime.date(today.year, 8, 20) # Fall semester
399
            may21 = datetime.date(today.year, 5, 21) # summer
400
            jan1  = datetime.date(today.year, 1,  1) # Spring semester
401
            # if they don't fall after those two, its fall
402

    
403
            if today > aug20:
404
               print "its fall semester!"
405
               #its fall, so they must have paid after may21 (either full year of half) to be paid
406
               qry3 = "SELECT civicrm_value_roboclub_info_2.date_paid_10 >= STR_TO_DATE('" + may21.strftime('%m/%d/%Y') + "', '%m/%d/%Y') ,  \
407
                       DATE_FORMAT(civicrm_value_roboclub_info_2.date_paid_10, '%M %d %Y') \
408
                       FROM civicrm_value_roboclub_info_2 \
409
                       WHERE civicrm_value_roboclub_info_2.card_number_1 = " + id.group(1) + ";"
410
               
411
            elif today > may21:
412
               print "its summer!"
413
               #its summer, so they need to have paid either full year since aug20 or one semester since jan 1
414
               qry3 = "SELECT (civicrm_value_roboclub_info_2.date_paid_10 >= STR_TO_DATE('" + aug20.strftime('%m/%d/%Y') + "', '%m/%d/%Y') AND \
415
                               civicrm_value_roboclub_info_2.amount_paid_11 = 25) OR \
416
                              (civicrm_value_roboclub_info_2.date_paid_10 >= STR_TO_DATE('" + jan1.strftime('%m/%d/%Y') + "', '%m/%d/%Y') AND \
417
                               civicrm_value_roboclub_info_2.amount_paid_11 = 15) , \
418
                       DATE_FORMAT(civicrm_value_roboclub_info_2.date_paid_10, '%M %d %Y') \
419
                       FROM civicrm_value_roboclub_info_2 \
420
                       WHERE civicrm_value_roboclub_info_2.card_number_1 = " + id.group(1) + ";"
421

    
422

    
423
               cursor.execute(qry3)
424
               result = cursor.fetchall()
425
               print result
426
            else:
427
               print "its spring!"
428
               #its spring, so they must have either paid full after aug20 or one semester since jan 1 (this is the same as summer)
429
               qry3 = "SELECT (civicrm_value_roboclub_info_2.date_paid_10 >= STR_TO_DATE('" + aug20.strftime('%m/%d/%Y') + "', '%m/%d/%Y') AND \
430
                               civicrm_value_roboclub_info_2.amount_paid_11 = 25) OR \
431
                              (civicrm_value_roboclub_info_2.date_paid_10 >= STR_TO_DATE('" + jan1.strftime('%m/%d/%Y') + "', '%m/%d/%Y') AND \
432
                               civicrm_value_roboclub_info_2.amount_paid_11 = 15) , \
433
                       DATE_FORMAT(civicrm_value_roboclub_info_2.date_paid_10, '%M %d %Y') \
434
                       FROM civicrm_value_roboclub_info_2 \
435
                       WHERE civicrm_value_roboclub_info_2.card_number_1 = 810797813"
436
#" + id.group(1) + ";"
437

    
438

    
439
            cursor.execute(qry3)
440
            result = cursor.fetchall()
441

    
442
            if len(result) == 0 or result[0][0] == 0 or result[0][0] == "NULL":
443
               print "DENIED: user has not paid recently enough!"
444
               if len(result[0]) >= 2:
445
                  print "date_paid: ",result[0][1]
446
                  logMessage(user + " has not paid since " + result[0][1] + ", revoking access!")
447
               else:
448
                  logMessage(user + " has not paid, revoking access!")
449
               acl = []
450

    
451

    
452
            # while we don't have a valid response
453
            # Note that this might already be set above when we were heoping for an ACK
454
            while resp==None:
455
               try:
456
                  resp = common.readKey()
457
               except common.TimeoutException:
458
                  print "got TIMEOUT from keypad, breaking out of loop"
459
                  break
460

    
461
               if idready == True:
462
                  #if someone swipes while waiting for keypad, ignore it
463
                  print "got swipe during transaction, ignoring"
464
               if time.time() - startTime > keypadTimeout:
465
                  print "KEYPAD TIMEOUT!"
466
                  logMessage("keypad timed out")
467
                  common.sendMessage(2,common.TT_TIMEOUT,'')
468
                  break
469

    
470
            #if we have a valid response
471
            #sometimes the FTDI chip seems to give a zero as a string when power is reset
472
            if resp != None and ord(resp) != 0 and resp != common.TT_TIMEOUT:
473

    
474
               toolName = reverseTool(resp)
475
               toolNameLong = toolName + " (k="+str(resp)+"("+str(ord(resp))+"), t="+str(keypad2toolID(resp))+")"
476
               print "request:",resp,"(",ord(resp),") ",toolNameLong
477

    
478
               if acl.count(resp) > 0:
479
                  common.sendAck(2)
480
                  time.sleep(0.5)
481
                  sendTool(resp)
482
                  print "ACCESS GRANTED"
483
                  logMessage(user+" ACCESSED tool "+toolNameLong)
484
                  if common.checkAck(resp):
485
                     print "ACK"
486
                     toolFails[toolName] = 0
487
                  else:
488
                     print "NO ACK!!!!"
489
                     logMessage("tool "+toolNameLong+" did not ACK")
490
                     toolFails[toolName] += 1
491
                     if toolFails[toolName] > common.MAX_TOOL_FAILS:
492
                        #TODO: send email
493
                        logMessage("WARNING: tool "+toolNameLong+
494
                                   " has failed to ACK "+
495
                                   str(common.MAX_TOOL_FAILS)+" times in a row.")
496

    
497
               else:
498
                  common.sendNack(2)
499
                  print "ACCESS DENIED!!"
500
                  logMessage(user + " DENIED on tool "+toolNameLong)
501

    
502
               clear_id()
503

    
504
            elif resp == common.TT_TIMEOUT:
505
               print "TIMEOUT sent from keypad"
506
               continue
507
            else:
508
               break
509
   except ctrlDException:
510
      print "got a ctrl-D, exiting cleanly"
511
      logMessage("NOTICE: tooltron going down because of ctrl-D press")
512
      
513
   finally:
514
      if not threadDone:
515
         print "stopping id reader thread..."
516
         idt.stop()