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() |