Project

General

Profile

Statistics
| Revision:

root / trunk / swipe / tooltron.py @ 213

History | View | Annotate | Download (11.5 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 serial
43
import MySQLdb
44
import getpass
45
from time import *
46

    
47
keypadTimeout = 11 #in seconds
48

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

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

    
77

    
78
TT_GET_KEY = 'k'
79
TT_ACK     = 'a'
80
TT_NACK    = 'n'
81
TT_TO      = 'f'
82
TT_TIMEOUT = 't'
83
TT_ON      = 'o'
84

    
85
BAUD_RATE = 9600
86

    
87
#fails until a warning is sent
88
MAX_TOOL_FAILS = 5
89

    
90
#################################################
91
#this code spawns a thread which always puts the last card id in the
92
#lastid variable.
93
#################################################
94
import threading
95
import time
96

    
97
lastid = None
98
idready = False
99

    
100
cv = threading.Condition()
101

    
102
class idThread ( threading.Thread ):
103

    
104
   def run ( self ):
105
       global lastid
106
       global idready
107

    
108
       try:
109
           while True:
110
               l = raw_input()
111
               cv.acquire()
112
               id = re.search('%([0-9]*)=.*', l)
113
               if id != None:
114
                   lastid = id
115
                   print "!!! setting idready"
116
                   idready = True
117
                   cv.notify()
118
               cv.release()
119

    
120
       except EOFError:
121
           cv.acquire()
122
           idready = True
123
           lastid=None
124
           cv.notify()
125
           cv.release()
126
#end thread
127
###############################################
128

    
129
flog = open("tooltron.log", "a")
130

    
131
def logMessage(str):
132
   flog.write(strftime("%Y.%m.%d %H:%M:%S ") + str + "\n")
133
   flog.flush()
134

    
135
logMessage("----- SERVER STARTED -----")
136

    
137
def keypad2toolID(t):
138
   if t in ids:
139
      return ids[t]
140
   else:
141
      return -1
142

    
143
def reverseTool(tn):
144
   for t in tools:
145
      if tn in tools[t]:
146
         return t
147

    
148
   return 'invalid'
149

    
150
#keep count of successive non-acks
151
toolFails = tools.copy()
152
for t in toolFails:
153
   toolFails[t] = 0
154

    
155
def get_last_id():
156
    global idready
157
    global lastid
158

    
159
    got = None
160

    
161
    print ">>> locking for last_id"
162

    
163
    cv.acquire()
164
    while not idready:
165
        print ">>> wait"
166
        cv.wait()
167

    
168
    print ">>> idready set"
169

    
170
    got = lastid
171
    idready = False
172
    cv.release()
173
    print ">>> done"
174

    
175
    return got
176

    
177
def clear_id():
178
    global idready
179
    global lastid
180

    
181
    print "=== locking to clear idready flag"
182
    cv.acquire()
183
    idready = False
184
    lastid = None
185
    cv.release()
186
    print "=== done"
187

    
188
    return
189

    
190

    
191
if len(sys.argv) < 2:
192
    print "usage: tooltron.py /path/to/bus/device"
193

    
194
else:
195

    
196
    pw = getpass.getpass("mysql password: ")
197
    db = MySQLdb.connect(host="roboclub8.frc.ri.cmu.edu", user="tooltron", passwd=pw, db="civicrm")
198
    print "connected, now accepting input"
199
    logMessage("connected to database")
200

    
201
    #start the id scan thread AFTER the getpass prompt
202
    idThread().start()
203

    
204
    cursor = db.cursor()
205

    
206
    qry = "SELECT tools_6 FROM civicrm_value_roboclub_info_2 WHERE card_number_1 = "
207

    
208
    bus = serial.Serial(sys.argv[1], BAUD_RATE, timeout = 2)
209
    bus.flushInput()
210
    print bus
211
    
212
    # ^ <src> <dest> <data>
213
    def sendTool(t):
214
        tn = keypad2toolID(t)
215
        msg = '^' + chr(1) + chr(tn) + TT_ON + chr(1 ^ tn ^ ord(TT_ON))
216
        print "seding power to tool ID",tn
217
        bus.write(msg)
218
        return
219

    
220
    def sendKeyRequest():
221
       tn = 2 #2 is the ID for the cardbox
222
       msg = '^' + chr(1) + chr(tn) + TT_GET_KEY + chr(1 ^ tn ^ ord(TT_GET_KEY))
223
       print "seding key request"
224
       bus.write(msg)
225
       return
226

    
227
    def sendAck(toolNum):
228
       msg = '^' + chr(1) + chr(toolNum) + TT_ACK + chr(1 ^ tn ^ ord(TT_ACK))
229
       print "seding ACK to",toolNum
230
       bus.write(msg)
231
       return
232

    
233
    def sendNack(toolNum):
234
       msg = '^' + chr(1) + chr(toolNum) + TT_NACK + chr(1 ^ tn ^ ord(TT_NACK))
235
       print "seding ACK to",toolNum
236
       bus.write(msg)
237
       return
238

    
239

    
240
    #returns key if the key_send packet is recived from tool ID 2
241
    # otherwise returns 0
242
    def readKey():
243
       startDelim = bus.read(1)
244
       if startDelim == '^':
245
          src = bus.read(1)
246
          if src == 0:
247
             return 0
248
          dest = bus.read(1)
249
          if dest == 0:
250
             return 0
251
          packetType = bus.read(1)
252
          if packetType == 0:
253
             return 0
254
          key = bus.read(1)
255
          if key == 0:
256
             return 0
257
          crc = bus.read(1)
258

    
259
          print "got packet"
260
          if src != 2 or dest != 1 or packetType != TT_KEY_SEND:
261
             print "Bad packet! ^",src,dest,packetType,key,crc
262

    
263
          if chr(ord(src) ^ ord(dest) ^ ord(packetType) ^ ord(key)) == crc:
264
             return key
265
          else:
266
             print "xor fail. got", crc, "should have been got",(chr(ord(src) ^ ord(dest) ^ ord(packetType) ^ ord(key)))
267

    
268
          return 0
269

    
270
       else:
271
          print "did not get start delim!: ", ord(startDelim)
272
          return 0
273
       
274
    #returns [src, data] or [] on error
275
    def readTool():
276
        ret = [0,0,0]
277

    
278
        if bus.read(1) == '^':
279
            ret[0] = bus.read(1)
280
            ret[1] = bus.read(1)
281
            ret[2] = bus.read(1)
282
            x = bus.read(1)
283

    
284
            print "got packet",ret
285

    
286
            if chr(ord(ret[0]) ^ ord(ret[1]) ^ ord(ret[2])) == x:
287
                return ret
288
            else:
289
                print "xor fail. got", x, "should have been got",(ord(ret[0]) ^ ord(ret[1]) ^ ord(ret[2]))
290

    
291
        return []
292

    
293
    def checkAck(t):
294
        tn = keypad2toolID(t)
295
        m = readTool()
296

    
297
        if m== []:
298
            return False
299

    
300

    
301
        return m[0] == chr(tn) and m[1] == chr(1) and m[2] == 'A'
302

    
303

    
304
    while True:
305

    
306
        id = get_last_id()
307
        
308
        if id != None:
309
            print "\n-----------\nid# ", id.group(1)
310

    
311
            sendKeyRequest()
312
            startTime = time.time()
313

    
314
            cursor.execute(qry + id.group(1))
315

    
316
            result = cursor.fetchall()
317

    
318
            acl = []
319

    
320
            for r in result:
321
                tls = r[0].split("\x01")
322
                for t in tls:
323
                    if t != '':
324
                        try:
325
                            acl.extend (tools[t])
326
                        except KeyError:
327
                            #this doesn't really matter
328
                            pass
329

    
330
            print "user has access to:", acl
331

    
332
            user = ""
333

    
334
            #query for name
335
            qry2 = "SELECT civicrm_contact.display_name \
336
                   FROM civicrm_value_roboclub_info_2 \
337
                   INNER JOIN (civicrm_contact) \
338
                   ON civicrm_value_roboclub_info_2.entity_id = civicrm_contact.id \
339
                   WHERE civicrm_value_roboclub_info_2.card_number_1 = " + id.group(1) + ";"
340

    
341
            cursor.execute(qry2)
342
            result = cursor.fetchall()
343
            
344
            if len(result) > 0:
345
               user += result[0][0]
346

    
347
            qry2 = "SELECT civicrm_email.email \
348
                   FROM civicrm_value_roboclub_info_2 \
349
                   INNER JOIN (civicrm_email) \
350
                   ON civicrm_email.contact_id = civicrm_value_roboclub_info_2.entity_id \
351
                   WHERE civicrm_value_roboclub_info_2.card_number_1 = " + id.group(1) + ";"
352

    
353
            cursor.execute(qry2)
354
            result = cursor.fetchall()
355
            
356
            if len(result) > 0:
357
               user += " ("+result[0][0]+")"
358

    
359
            if user == "":
360
               user = str(id.group(1))
361

    
362
            print user
363

    
364

    
365
            resp = 0
366
            while resp==0:
367
               resp = readKey()
368

    
369
               #no lock since we are just reading and we can afford to
370
               #miss a loop
371
               if idready == True:
372
                  #if someone swipes, break out of the loop
373
                  print "**swipe during read, bailing out"
374
                  logMessage("user swipped during keypad read")
375
                  break
376
               if time.time() - startTime > keypadTimeout:
377
                  print "KEYPAD TIMEOUT!"
378
                  logMessage("keypad timed out")
379
                  break
380

    
381
            #if we have a valid response
382
            #sometimes the FTDI chip seems to give a zero as a string when power is reset
383
            if resp != "" and ord(resp) != 0 and resp != TT_TIMEOUT:
384

    
385
               toolName = reverseTool(resp)
386
               toolNameLong = toolName + " (k="+str(resp)+"("+str(ord(resp))+"), t="+str(keypad2toolID(resp))+")"
387
               print "request:",resp,"(",ord(resp),") ",toolNameLong
388

    
389
               if acl.count(resp) > 0:
390
                  sendAck(2)
391
                  sendTool(resp)
392
                  print "ACCESS GRANTED"
393
                  logMessage(user+" ACCESSED tool "+toolNameLong)
394
                  if checkAck(resp):
395
                     print "ACK"
396
                     toolFails[toolName] = 0
397
                  else:
398
                     print "NO ACK!!!!"
399
                     logMessage("tool "+toolNameLong+" did not ACK")
400
                     toolFails[toolName] += 1
401
                     if toolFails[toolName] > MAX_TOOL_FAILS:
402
                        #TODO: send email
403
                        logMessage("WARNING: tool "+toolNameLong+
404
                                   " has failed to ACK "+
405
                                   str(MAX_TOOL_FAILS)+" times in a row.")
406

    
407
               else:
408
                  sendNack(2)
409
                  print "ACCESS DENIED!!"
410
                  logMessage(user + " DENIED on tool "+toolNameLong)
411

    
412
               clear_id()
413

    
414
        elif resp == 0 or ord(resp) == 0: #if we get noise, send timeout to reset and sync states
415
           keypad.write(TT_TIMEOUT)
416
           print "ERROR: got strange byte, timing out"
417
           logMessage("ERROR: got 0 as a byte, sending timeout (cardbox power issue?)")
418
        else:
419
            break