Project

General

Profile

Statistics
| Revision:

root / trunk / swipe / tooltron.py @ 210

History | View | Annotate | Download (10.4 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
keypad = []
48
keypadTimeout = 11 #in seconds
49

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

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

    
78

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

    
86
BAUD_RATE = 9600
87

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

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

    
98
lastid = None
99
idready = False
100

    
101
cv = threading.Condition()
102

    
103
class idThread ( threading.Thread ):
104

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

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

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

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

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

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

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

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

    
149
   return 'invalid'
150

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

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

    
160
    got = None
161

    
162
    print ">>> locking for last_id"
163

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

    
169
    print ">>> idready set"
170

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

    
176
    return got
177

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

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

    
189
    return
190

    
191

    
192
if len(sys.argv) < 3:
193
    print "usage: tooltron.py /path/to/keypad/device /path/to/tool/bus/device"
194

    
195
else:
196

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

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

    
205
    cursor = db.cursor()
206

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

    
209
    # user has 10 seconds to press a key, after that the keypad device
210
    # should send a timeout packet. This is a non-blocking serial
211
    # device so we can re-transmit the key request when the user
212
    # swipes a card instead of being stuck on a blocking read call
213
    keypad = serial.Serial(sys.argv[1], BAUD_RATE, timeout = 0) 
214
    keypad.flushInput()
215
    print keypad
216

    
217
    bus = serial.Serial(sys.argv[2], BAUD_RATE, timeout = 2)
218
    bus.flushInput()
219
    print bus
220
    
221
    # ^ <src> <dest> <data>
222
    def sendTool(t):
223
        tn = keypad2toolID(t)
224
        msg = '^' + chr(1) + chr(tn) + TT_ON + chr(1 ^ tn ^ ord(TT_ON))
225
        print "seding power to tool ID",tn
226
        bus.write(msg)
227
        return
228

    
229
    #returns [src, data] or [] on error
230
    def readTool():
231
        ret = [0,0,0]
232

    
233
        if bus.read(1) == '^':
234
            ret[0] = bus.read(1)
235
            ret[1] = bus.read(1)
236
            ret[2] = bus.read(1)
237
            x = bus.read(1)
238

    
239
            print "got packet",ret
240

    
241
            if chr(ord(ret[0]) ^ ord(ret[1]) ^ ord(ret[2])) == x:
242
                return ret
243
            else:
244
                print "xor fail. got", x, "should have been got",(ord(ret[0]) ^ ord(ret[1]) ^ ord(ret[2]))
245

    
246
        return []
247

    
248
    def checkAck(t):
249
        tn = keypad2toolID(t)
250
        m = readTool()
251

    
252
        if m== []:
253
            return False
254

    
255

    
256
        return m[0] == chr(tn) and m[1] == chr(1) and m[2] == 'A'
257

    
258

    
259
    while True:
260

    
261
        id = get_last_id()
262
        
263
        if id != None:
264
            print "\n-----------\nid# ", id.group(1)
265

    
266

    
267
            print "sending key request"
268
            keypad.flushInput()
269
            keypad.write(TT_GET_KEY)
270

    
271
            cursor.execute(qry + id.group(1))
272

    
273
            result = cursor.fetchall()
274

    
275
            acl = []
276

    
277
            for r in result:
278
                tls = r[0].split("\x01")
279
                for t in tls:
280
                    if t != '':
281
                        try:
282
                            acl.extend (tools[t])
283
                        except KeyError:
284
                            #this doesn't really matter
285
                            pass
286

    
287
            print "user has access to:", acl
288

    
289
            user = ""
290

    
291
            #query for name
292
            qry2 = "SELECT civicrm_contact.display_name \
293
                   FROM civicrm_value_roboclub_info_2 \
294
                   INNER JOIN (civicrm_contact) \
295
                   ON civicrm_value_roboclub_info_2.entity_id = civicrm_contact.id \
296
                   WHERE civicrm_value_roboclub_info_2.card_number_1 = " + id.group(1) + ";"
297

    
298
            cursor.execute(qry2)
299
            result = cursor.fetchall()
300
            
301
            if len(result) > 0:
302
               user += result[0][0]
303

    
304
            qry2 = "SELECT civicrm_email.email \
305
                   FROM civicrm_value_roboclub_info_2 \
306
                   INNER JOIN (civicrm_email) \
307
                   ON civicrm_email.contact_id = civicrm_value_roboclub_info_2.entity_id \
308
                   WHERE civicrm_value_roboclub_info_2.card_number_1 = " + id.group(1) + ";"
309

    
310
            cursor.execute(qry2)
311
            result = cursor.fetchall()
312
            
313
            if len(result) > 0:
314
               user += " ("+result[0][0]+")"
315

    
316
            if user == "":
317
               user = str(id.group(1))
318

    
319
            print user
320

    
321
            startTime = time.time();
322
            resp = ""
323
            while resp=="":
324
               resp = keypad.read(1)
325
               #no lock since we are just reading and we can afford to
326
               #miss a loop
327
               if idready == True:
328
                  #if someone swipes, break out of the loop
329
                  print "**swipe during read, bailing out"
330
                  logMessage("user swipped during keypad read")
331
                  break
332
               if time.time() - startTime > keypadTimeout:
333
                  print "KEYPAD TIMEOUT!"
334
                  logMessage("keypad timed out")
335
                  break
336

    
337
            #if we have a vaid response
338
            #sometimes the FTDI chip seems to give a zero as a string when power is reset
339
            if resp != "" and ord(resp) != 0 and resp != TT_TIMEOUT:
340

    
341
               toolName = reverseTool(resp)
342
               toolNameLong = toolName + " (k="+str(resp)+"("+str(ord(resp))+"), t="+str(keypad2toolID(resp))+")"
343
               print "request:",resp,"(",ord(resp),") ",toolNameLong
344

    
345
               if acl.count(resp) > 0:
346
                  keypad.write(TT_ACK)
347
                  sendTool(resp)
348
                  print "ACCESS GRANTED"
349
                  logMessage(user+" ACCESSED tool "+toolNameLong)
350
                  if checkAck(resp):
351
                     print "ACK"
352
                     toolFails[toolName] = 0
353
                  else:
354
                     print "NO ACK!!!!"
355
                     logMessage("tool "+toolNameLong+" did not ACK")
356
                     toolFails[toolName] += 1
357
                     if toolFails[toolName] > MAX_TOOL_FAILS:
358
                        #TODO: send email
359
                        logMessage("WARNING: tool "+toolNameLong+
360
                                   " has failed to ACK "+
361
                                   str(MAX_TOOL_FAILS)+" times in a row.")
362

    
363
               else:
364
                  keypad.write(TT_NACK)
365
                  print "ACCESS DENIED!!"
366
                  logMessage(user + " DENIED on tool "+toolNameLong)
367

    
368
               clear_id()
369

    
370
        elif resp == 0 or ord(resp) == 0: #if we get noise, send timeout to reset and sync states
371
           keypad.write(TT_TIMEOUT);
372
           print "ERROR: got strange byte, timing out"
373
           logMessage("ERROR: got 0 as a byte, sending timeout (cardbox power issue?)")
374
        else:
375
            break