root / trunk / swipe / tooltron.py @ 283
History | View | Annotate | Download (10.1 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 sys |
| 41 | import MySQLdb |
| 42 | import getpass |
| 43 | import datetime |
| 44 | from datetime import date |
| 45 | from time import * |
| 46 | sys.path.append('../common')
|
| 47 | import common |
| 48 | |
| 49 | keypadTimeout = 12 #in seconds |
| 50 | |
| 51 | #this table maps which keypad button to press for each tool. This |
| 52 | #list should probably be printed next to the swipe box |
| 53 | tools = {
|
| 54 | 'Bandsaw':[], |
| 55 | 'DrillPress':['3','8'], |
| 56 | 'Mill':['4'], |
| 57 | 'Lathe':[], |
| 58 | #HACK: since the saw isn't used I use it for testing random boards |
| 59 | 'ChopMiterSaw':['1','2','5','6','7','9'] |
| 60 | } |
| 61 | |
| 62 | #this table maps keypad button numbers to tool IDs. The default is |
| 63 | #that the tool id is 10+the keypad number, but this would change if |
| 64 | #boards were swapped out. The tool ID number should be printed on the |
| 65 | #tool board, but needs to be hand coded into the toolbox code |
| 66 | ids = {
|
| 67 | '1':11, |
| 68 | '2':12, |
| 69 | '3':13, |
| 70 | '4':14, |
| 71 | '5':15, |
| 72 | '6':16, |
| 73 | '7':17, |
| 74 | '8':18, |
| 75 | '9':19, |
| 76 | '0':13 |
| 77 | } |
| 78 | |
| 79 | try: |
| 80 | flog = open("/var/log/tooltron", "a")
|
| 81 | except IOError as e: |
| 82 | print "\n*********************************************" |
| 83 | print "File /var/log/tooltron could not be opened!" |
| 84 | print e |
| 85 | print "Using local file 'tooltron.log'" |
| 86 | print "*********************************************\n" |
| 87 | flog = open("tooltron.log","a")
|
| 88 | finally: |
| 89 | print flog |
| 90 | |
| 91 | def logMessage(str): |
| 92 | flog.write(strftime("%Y.%m.%d %H:%M:%S ") + str + "\n")
|
| 93 | flog.flush() |
| 94 | |
| 95 | logMessage("----- SERVER STARTED -----")
|
| 96 | |
| 97 | def keypad2toolID(t): |
| 98 | if t in ids: |
| 99 | return ids[t] |
| 100 | else: |
| 101 | return -1 |
| 102 | |
| 103 | def reverseTool(tn): |
| 104 | for t in tools: |
| 105 | if tn in tools[t]: |
| 106 | return t |
| 107 | |
| 108 | return 'invalid' |
| 109 | |
| 110 | #keep count of successive non-acks |
| 111 | toolFails = tools.copy() |
| 112 | for t in toolFails: |
| 113 | toolFails[t] = 0 |
| 114 | |
| 115 | |
| 116 | ################################################################################ |
| 117 | # Main: |
| 118 | |
| 119 | if len(sys.argv) < 2: |
| 120 | print "usage: tooltron.py /path/to/bus/device" |
| 121 | exit(-1) |
| 122 | |
| 123 | if not common.initBus(sys.argv[1]): |
| 124 | print "usage: tooltron.py /path/to/bus/device" |
| 125 | else: |
| 126 | |
| 127 | pw = getpass.getpass("mysql password: ")
|
| 128 | |
| 129 | cursor = None |
| 130 | qry1 = "SELECT tools_6 FROM civicrm_value_roboclub_info_2 WHERE card_number_1 = " |
| 131 | |
| 132 | # wapper to translate tool number |
| 133 | def sendTool(t): |
| 134 | tn = keypad2toolID(t) |
| 135 | common.sendTool(tn) |
| 136 | |
| 137 | # This function eats the rest of the packet where data is what we have so far |
| 138 | # TODO: do I still need this? |
| 139 | def flushPacket(data): |
| 140 | while len(data) < 6: |
| 141 | data += bus.read(1) |
| 142 | |
| 143 | plen = data[4] |
| 144 | if plen > 0: |
| 145 | bus.read(ord(plen)) |
| 146 | |
| 147 | while True: |
| 148 | |
| 149 | x = common.readTransaction() |
| 150 | |
| 151 | print "\n" |
| 152 | |
| 153 | if x == None: |
| 154 | print "ERROR: bad transaction" |
| 155 | logMessage("ERROR: got bad packet from cardbox")
|
| 156 | continue |
| 157 | |
| 158 | [key, cardnum] = x |
| 159 | |
| 160 | print "\--------------------\n",strftime("%Y.%m.%d %H:%M:%S \n")
|
| 161 | print "cardnum:", cardnum |
| 162 | print "key:", key |
| 163 | |
| 164 | # Start doing the MySQL queries, hope this isn't too slow |
| 165 | |
| 166 | print "running queries..." |
| 167 | |
| 168 | if cursor != None: |
| 169 | cursor.close() |
| 170 | |
| 171 | #TODO: handle timeouts / slow connections! |
| 172 | db = MySQLdb.connect(host="roboclub8.frc.ri.cmu.edu", user="tooltron", passwd=pw, db="civicrm") |
| 173 | print "connected to mysql server" |
| 174 | cursor = db.cursor() |
| 175 | print "got db cursor" |
| 176 | |
| 177 | print "qry1 (ACL)..." |
| 178 | cursor.execute(qry1 + cardnum) |
| 179 | |
| 180 | result = cursor.fetchall() |
| 181 | |
| 182 | acl = [] |
| 183 | |
| 184 | for r in result: |
| 185 | tls = r[0].split("\x01")
|
| 186 | for t in tls: |
| 187 | if t != '': |
| 188 | try: |
| 189 | acl.extend (tools[t]) |
| 190 | except KeyError: |
| 191 | #this doesn't really matter |
| 192 | pass |
| 193 | |
| 194 | print "user has access to:", acl |
| 195 | |
| 196 | user = "" |
| 197 | |
| 198 | print "qry2 (name/email)..." |
| 199 | #query for name |
| 200 | qry2 = "SELECT civicrm_contact.display_name \ |
| 201 | FROM civicrm_value_roboclub_info_2 \ |
| 202 | INNER JOIN (civicrm_contact) \ |
| 203 | ON civicrm_value_roboclub_info_2.entity_id = civicrm_contact.id \ |
| 204 | WHERE civicrm_value_roboclub_info_2.card_number_1 = " + cardnum + ";" |
| 205 | |
| 206 | cursor.execute(qry2) |
| 207 | result = cursor.fetchall() |
| 208 | |
| 209 | if len(result) > 0: |
| 210 | user += result[0][0] |
| 211 | |
| 212 | qry2 = "SELECT civicrm_email.email \ |
| 213 | FROM civicrm_value_roboclub_info_2 \ |
| 214 | INNER JOIN (civicrm_email) \ |
| 215 | ON civicrm_email.contact_id = civicrm_value_roboclub_info_2.entity_id \ |
| 216 | WHERE civicrm_value_roboclub_info_2.card_number_1 = " + cardnum + ";" |
| 217 | |
| 218 | cursor.execute(qry2) |
| 219 | result = cursor.fetchall() |
| 220 | |
| 221 | if len(result) > 0: |
| 222 | user += " ("+result[0][0]+")"
|
| 223 | |
| 224 | if user == "": |
| 225 | user = cardnum |
| 226 | |
| 227 | print "[[[",user,"]]]" |
| 228 | |
| 229 | print "qry3 (payment)..." |
| 230 | # check if member has paid recently |
| 231 | # first, figure out what semester it is |
| 232 | today = date.today() |
| 233 | |
| 234 | # semester start dates |
| 235 | aug20 = datetime.date(today.year, 8, 20) # Fall semester |
| 236 | may21 = datetime.date(today.year, 5, 21) # summer |
| 237 | jan1 = datetime.date(today.year, 1, 1) # Spring semester |
| 238 | # if they don't fall after those two, its fall |
| 239 | |
| 240 | if today > aug20: |
| 241 | print "its fall semester!" |
| 242 | #its fall, so they must have paid after may21 (either full year of half) to be paid |
| 243 | qry3 = "SELECT civicrm_value_roboclub_info_2.date_paid_10 >= STR_TO_DATE('" + may21.strftime('%m/%d/%Y') + "', '%m/%d/%Y') , \
|
| 244 | DATE_FORMAT(civicrm_value_roboclub_info_2.date_paid_10, '%M %d %Y') \ |
| 245 | FROM civicrm_value_roboclub_info_2 \ |
| 246 | WHERE civicrm_value_roboclub_info_2.card_number_1 = " + cardnum + ";" |
| 247 | |
| 248 | elif today > may21: |
| 249 | print "its summer!" |
| 250 | #its summer, so they need to have paid either full year since aug20 or one semester since jan 1 |
| 251 | qry3 = "SELECT (civicrm_value_roboclub_info_2.date_paid_10 >= STR_TO_DATE('" + aug20.strftime('%m/%d/%Y') + "', '%m/%d/%Y') AND \
|
| 252 | civicrm_value_roboclub_info_2.amount_paid_11 = 25) OR \ |
| 253 | (civicrm_value_roboclub_info_2.date_paid_10 >= STR_TO_DATE('" + jan1.strftime('%m/%d/%Y') + "', '%m/%d/%Y') AND \
|
| 254 | civicrm_value_roboclub_info_2.amount_paid_11 = 15) , \ |
| 255 | DATE_FORMAT(civicrm_value_roboclub_info_2.date_paid_10, '%M %d %Y') \ |
| 256 | FROM civicrm_value_roboclub_info_2 \ |
| 257 | WHERE civicrm_value_roboclub_info_2.card_number_1 = " + cardnum + ";" |
| 258 | else: |
| 259 | print "its spring!" |
| 260 | #its spring, so they must have either paid full after aug20 or one semester since jan 1 (this is the same as summer) |
| 261 | qry3 = "SELECT (civicrm_value_roboclub_info_2.date_paid_10 >= STR_TO_DATE('" + aug20.strftime('%m/%d/%Y') + "', '%m/%d/%Y') AND \
|
| 262 | civicrm_value_roboclub_info_2.amount_paid_11 = 25) OR \ |
| 263 | (civicrm_value_roboclub_info_2.date_paid_10 >= STR_TO_DATE('" + jan1.strftime('%m/%d/%Y') + "', '%m/%d/%Y') AND \
|
| 264 | civicrm_value_roboclub_info_2.amount_paid_11 = 15) , \ |
| 265 | DATE_FORMAT(civicrm_value_roboclub_info_2.date_paid_10, '%M %d %Y') \ |
| 266 | FROM civicrm_value_roboclub_info_2 \ |
| 267 | WHERE civicrm_value_roboclub_info_2.card_number_1 = " + cardnum + ";" |
| 268 | |
| 269 | |
| 270 | cursor.execute(qry3) |
| 271 | result = cursor.fetchall() |
| 272 | |
| 273 | if len(result) == 0 or result[0][0] == 0 or result[0][0] == "NULL": |
| 274 | print "DENIED: user has not paid recently enough!" |
| 275 | if len(result[0]) >= 2: |
| 276 | print "date_paid: ",result[0][1] |
| 277 | logMessage(user + " has not paid since " + result[0][1] + ", revoking access!") |
| 278 | else: |
| 279 | logMessage(user + " has not paid, revoking access!") |
| 280 | #acl = [] #TODO: uncomment! |
| 281 | |
| 282 | |
| 283 | toolName = reverseTool(key) |
| 284 | toolNameLong = toolName + " (k="+str(key)+"("+str(ord(key))+"), t="+str(keypad2toolID(key))+")"
|
| 285 | print "request:",key,"(",ord(key),") ",toolNameLong
|
| 286 | |
| 287 | if acl.count(key) > 0: |
| 288 | common.sendGrant() |
| 289 | #time.sleep(0.5) #TODO: do I still need this? |
| 290 | sendTool(key) |
| 291 | print "ACCESS GRANTED" |
| 292 | logMessage(user+" ACCESSED tool "+toolNameLong) |
| 293 | if common.checkAck(key): |
| 294 | print "ACK" |
| 295 | toolFails[toolName] = 0 |
| 296 | else: |
| 297 | print "NO ACK!!!!" |
| 298 | logMessage("tool "+toolNameLong+" did not ACK")
|
| 299 | toolFails[toolName] += 1 |
| 300 | if toolFails[toolName] > common.MAX_TOOL_FAILS: |
| 301 | #TODO: send email |
| 302 | logMessage("WARNING: tool "+toolNameLong+
|
| 303 | " has failed to ACK "+ |
| 304 | str(common.MAX_TOOL_FAILS)+" times in a row.") |
| 305 | |
| 306 | else: |
| 307 | common.sendDeny() |
| 308 | print "ACCESS DENIED!!" |
| 309 | logMessage(user + " DENIED on tool "+toolNameLong) |