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 |