root / trunk / swipe / tooltron.py @ 235
History | View | Annotate | Download (11.5 KB)
1 | 140 | bneuman | #!/usr/bin/python
|
---|---|---|---|
2 | |||
3 | 139 | kwoo | """
|
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 | 99 | bneuman | |
22 | 125 | bneuman | ################################################################################
|
23 | 147 | bneuman | ## tooltron.py is the main tooltron server
|
24 | 125 | bneuman | # 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 | 128 | bneuman | # 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 | 125 | bneuman | # email bradneuman@gmail.com if you have any questions
|
37 | ################################################################################
|
||
38 | |||
39 | |||
40 | 99 | bneuman | import re |
41 | import sys |
||
42 | 119 | bneuman | import MySQLdb |
43 | import getpass |
||
44 | 144 | bneuman | from time import * |
45 | 224 | bneuman | sys.path.append('../common')
|
46 | 222 | bneuman | import common |
47 | |||
48 | 234 | bneuman | keypadTimeout = 31 #in seconds |
49 | 119 | bneuman | |
50 | 143 | bneuman | #this table maps which keypad button to press for each tool. This
|
51 | #list should probably be printed next to the swipe box
|
||
52 | 130 | bneuman | tools = { |
53 | 150 | bneuman | 'Bandsaw':[],
|
54 | 'DrillPress':['3','8'], |
||
55 | 130 | bneuman | 'Mill':['4'], |
56 | 150 | bneuman | 'Lathe':[],
|
57 | 143 | bneuman | #HACK: since the saw isn't used I use it for testing random boards
|
58 | 150 | bneuman | 'ChopMiterSaw':['1','2','5','6','7','9'] |
59 | 130 | bneuman | } |
60 | |||
61 | 143 | bneuman | #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 | 130 | bneuman | |
78 | 128 | bneuman | #################################################
|
79 | #this code spawns a thread which always puts the last card id in the
|
||
80 | #lastid variable.
|
||
81 | #################################################
|
||
82 | import threading |
||
83 | import time |
||
84 | 230 | bneuman | import select |
85 | import os |
||
86 | 128 | bneuman | |
87 | lastid = None
|
||
88 | idready = False
|
||
89 | 230 | bneuman | threadDone = False
|
90 | threadDoneExcp = None
|
||
91 | 128 | bneuman | |
92 | cv = threading.Condition() |
||
93 | |||
94 | 230 | bneuman | class ctrlDException (Exception): |
95 | def __init__(self,str): |
||
96 | Exception.__init__(self) |
||
97 | self.str = str |
||
98 | |||
99 | def __str__(self): |
||
100 | return "Ctrl-D pressed: " + self.str |
||
101 | |||
102 | 128 | bneuman | class idThread ( threading.Thread ): |
103 | |||
104 | 230 | bneuman | def __init__ (self): |
105 | threading.Thread.__init__(self)
|
||
106 | self.wpipe = None |
||
107 | |||
108 | def stop (self): |
||
109 | self.stop_thread = True |
||
110 | if self.wpipe != None: |
||
111 | # write to this pipe so that the select will drop out
|
||
112 | self.wpipe.write('!') |
||
113 | |||
114 | def run (self): |
||
115 | 128 | bneuman | global lastid
|
116 | global idready
|
||
117 | 230 | bneuman | global threadDone
|
118 | global threadDoneExcp
|
||
119 | self.stop_thread = False |
||
120 | 128 | bneuman | |
121 | 230 | bneuman | [r,w] = os.pipe() |
122 | self.rpipe = os.fdopen(r,'r') |
||
123 | self.wpipe = os.fdopen(w,'w') |
||
124 | |||
125 | 128 | bneuman | try:
|
126 | 230 | bneuman | while not self.stop_thread: |
127 | # hang until we either get keyboard input or are told to stop
|
||
128 | [active,a,b] = select.select([sys.stdin,self.rpipe],[],[])
|
||
129 | if not self.stop_thread: |
||
130 | l = raw_input()
|
||
131 | cv.acquire() |
||
132 | try:
|
||
133 | id = re.search('%([0-9]*)=.*', l)
|
||
134 | if id != None: |
||
135 | lastid = id
|
||
136 | print "!!! setting idready" |
||
137 | idready = True
|
||
138 | cv.notify() |
||
139 | else:
|
||
140 | ifready = False
|
||
141 | finally:
|
||
142 | cv.release() |
||
143 | print "thread stopped" |
||
144 | 128 | bneuman | |
145 | except EOFError: |
||
146 | cv.acquire() |
||
147 | idready = True
|
||
148 | lastid=None
|
||
149 | cv.notify() |
||
150 | cv.release() |
||
151 | 230 | bneuman | threadDoneExcp=ctrlDException("thread exiting cleanly")
|
152 | except Exception, e: |
||
153 | threadDoneExcp = e #TODO: provide a line number or something?
|
||
154 | finally:
|
||
155 | print "id thread getting ready to exit..." |
||
156 | threadDone = True
|
||
157 | cv.acquire() |
||
158 | idready = False
|
||
159 | cv.notify() |
||
160 | cv.release() |
||
161 | print "id thread exiting" |
||
162 | |||
163 | 128 | bneuman | #end thread
|
164 | ###############################################
|
||
165 | |||
166 | 145 | bneuman | flog = open("tooltron.log", "a") |
167 | 144 | bneuman | |
168 | def logMessage(str): |
||
169 | flog.write(strftime("%Y.%m.%d %H:%M:%S ") + str + "\n") |
||
170 | 146 | bneuman | flog.flush() |
171 | 144 | bneuman | |
172 | 145 | bneuman | logMessage("----- SERVER STARTED -----")
|
173 | |||
174 | 141 | bneuman | def keypad2toolID(t): |
175 | 145 | bneuman | if t in ids: |
176 | return ids[t]
|
||
177 | else:
|
||
178 | return -1 |
||
179 | 141 | bneuman | |
180 | 144 | bneuman | def reverseTool(tn): |
181 | for t in tools: |
||
182 | if tn in tools[t]: |
||
183 | return t
|
||
184 | |||
185 | 145 | bneuman | return 'invalid' |
186 | 144 | bneuman | |
187 | 145 | bneuman | #keep count of successive non-acks
|
188 | toolFails = tools.copy() |
||
189 | for t in toolFails: |
||
190 | toolFails[t] = 0
|
||
191 | |||
192 | 128 | bneuman | def get_last_id(): |
193 | global idready
|
||
194 | global lastid
|
||
195 | 230 | bneuman | global threadDoneExcp
|
196 | 128 | bneuman | |
197 | got = None
|
||
198 | |||
199 | print ">>> locking for last_id" |
||
200 | |||
201 | cv.acquire() |
||
202 | while not idready: |
||
203 | 230 | bneuman | if threadDoneExcp != None: |
204 | print "thread has been exited" |
||
205 | raise threadDoneExcp
|
||
206 | 128 | bneuman | |
207 | 230 | bneuman | print ">>> wait" |
208 | cv.wait() |
||
209 | |||
210 | 128 | bneuman | print ">>> idready set" |
211 | |||
212 | got = lastid |
||
213 | idready = False
|
||
214 | cv.release() |
||
215 | print ">>> done" |
||
216 | |||
217 | return got
|
||
218 | |||
219 | def clear_id(): |
||
220 | global idready
|
||
221 | global lastid
|
||
222 | |||
223 | print "=== locking to clear idready flag" |
||
224 | cv.acquire() |
||
225 | idready = False
|
||
226 | lastid = None
|
||
227 | cv.release() |
||
228 | print "=== done" |
||
229 | |||
230 | return
|
||
231 | |||
232 | 222 | bneuman | if len(sys.argv) < 2: |
233 | print "usage: tooltron.py /path/to/bus/device" |
||
234 | exit(-1) |
||
235 | 128 | bneuman | |
236 | 222 | bneuman | if not common.initBus(sys.argv[1]): |
237 | 213 | bneuman | print "usage: tooltron.py /path/to/bus/device" |
238 | 99 | bneuman | else:
|
239 | |||
240 | 230 | bneuman | pw = getpass.getpass("mysql password: ")
|
241 | db = MySQLdb.connect(host="roboclub8.frc.ri.cmu.edu", user="tooltron", passwd=pw, db="civicrm") |
||
242 | print "connected, now accepting input" |
||
243 | logMessage("connected to database")
|
||
244 | 119 | bneuman | |
245 | 230 | bneuman | #start the id scan thread AFTER the getpass prompt
|
246 | idt = idThread() |
||
247 | idt.start() |
||
248 | |||
249 | # If there are any uncaught exceptions in the rest of the code, we need to kill the id thread
|
||
250 | # so the program as a whole will exit
|
||
251 | try:
|
||
252 | cursor = db.cursor() |
||
253 | 130 | bneuman | |
254 | 230 | bneuman | qry = "SELECT tools_6 FROM civicrm_value_roboclub_info_2 WHERE card_number_1 = "
|
255 | 119 | bneuman | |
256 | 230 | bneuman | def sendTool(t): |
257 | tn = keypad2toolID(t) |
||
258 | common.sendTool(tn) |
||
259 | 120 | bneuman | |
260 | 230 | bneuman | # This function eats the rest of the packet where data is what we have so far
|
261 | def flushPacket(data): |
||
262 | while len(data) < 6: |
||
263 | data += bus.read(1)
|
||
264 | 221 | bneuman | |
265 | 230 | bneuman | plen = data[4]
|
266 | if plen > 0: |
||
267 | bus.read(ord(plen))
|
||
268 | 221 | bneuman | |
269 | 230 | bneuman | while True: |
270 | id = get_last_id() |
||
271 | 128 | bneuman | |
272 | 230 | bneuman | if id != None: |
273 | 125 | bneuman | print "\n-----------\nid# ", id.group(1) |
274 | 99 | bneuman | |
275 | 222 | bneuman | common.sendKeyRequest() |
276 | 213 | bneuman | startTime = time.time() |
277 | 119 | bneuman | |
278 | cursor.execute(qry + id.group(1)) |
||
279 | |||
280 | result = cursor.fetchall() |
||
281 | |||
282 | acl = [] |
||
283 | |||
284 | for r in result: |
||
285 | 230 | bneuman | tls = r[0].split("\x01") |
286 | for t in tls: |
||
287 | if t != '': |
||
288 | try:
|
||
289 | acl.extend (tools[t]) |
||
290 | except KeyError: |
||
291 | #this doesn't really matter
|
||
292 | pass
|
||
293 | 119 | bneuman | |
294 | print "user has access to:", acl |
||
295 | 125 | bneuman | |
296 | 146 | bneuman | user = ""
|
297 | 145 | bneuman | |
298 | 146 | bneuman | #query for name
|
299 | qry2 = "SELECT civicrm_contact.display_name \
|
||
300 | 230 | bneuman | FROM civicrm_value_roboclub_info_2 \
|
301 | INNER JOIN (civicrm_contact) \
|
||
302 | ON civicrm_value_roboclub_info_2.entity_id = civicrm_contact.id \
|
||
303 | WHERE civicrm_value_roboclub_info_2.card_number_1 = " + id.group(1) + ";" |
||
304 | 146 | bneuman | |
305 | cursor.execute(qry2) |
||
306 | result = cursor.fetchall() |
||
307 | 230 | bneuman | |
308 | 146 | bneuman | if len(result) > 0: |
309 | user += result[0][0] |
||
310 | |||
311 | qry2 = "SELECT civicrm_email.email \
|
||
312 | 230 | bneuman | FROM civicrm_value_roboclub_info_2 \
|
313 | INNER JOIN (civicrm_email) \
|
||
314 | ON civicrm_email.contact_id = civicrm_value_roboclub_info_2.entity_id \
|
||
315 | WHERE civicrm_value_roboclub_info_2.card_number_1 = " + id.group(1) + ";" |
||
316 | 146 | bneuman | |
317 | cursor.execute(qry2) |
||
318 | result = cursor.fetchall() |
||
319 | 230 | bneuman | |
320 | 146 | bneuman | if len(result) > 0: |
321 | user += " ("+result[0][0]+")" |
||
322 | |||
323 | if user == "": |
||
324 | user = str(id.group(1)) |
||
325 | |||
326 | print user
|
||
327 | |||
328 | 213 | bneuman | |
329 | 222 | bneuman | resp = None
|
330 | while resp==None: |
||
331 | 235 | bneuman | try:
|
332 | resp = common.readKey() |
||
333 | except TimoutException:
|
||
334 | print "got TIMEOUT from keypad, breaking out of loop" |
||
335 | break
|
||
336 | 213 | bneuman | |
337 | 143 | bneuman | #no lock since we are just reading and we can afford to
|
338 | #miss a loop
|
||
339 | if idready == True: |
||
340 | #if someone swipes, break out of the loop
|
||
341 | print "**swipe during read, bailing out" |
||
342 | 144 | bneuman | logMessage("user swipped during keypad read")
|
343 | 143 | bneuman | break
|
344 | if time.time() - startTime > keypadTimeout:
|
||
345 | print "KEYPAD TIMEOUT!" |
||
346 | 144 | bneuman | logMessage("keypad timed out")
|
347 | 234 | bneuman | common.sendMessage(2,common.TT_TIMEOUT,'') |
348 | 143 | bneuman | break
|
349 | 99 | bneuman | |
350 | 230 | bneuman | #if we have a valid response
|
351 | #sometimes the FTDI chip seems to give a zero as a string when power is reset
|
||
352 | if resp != None and ord(resp) != 0 and resp != common.TT_TIMEOUT: |
||
353 | 99 | bneuman | |
354 | 230 | bneuman | toolName = reverseTool(resp) |
355 | toolNameLong = toolName + " (k="+str(resp)+"("+str(ord(resp))+"), t="+str(keypad2toolID(resp))+")" |
||
356 | print "request:",resp,"(",ord(resp),") ",toolNameLong |
||
357 | 119 | bneuman | |
358 | 230 | bneuman | if acl.count(resp) > 0: |
359 | common.sendAck(2)
|
||
360 | sendTool(resp) |
||
361 | print "ACCESS GRANTED" |
||
362 | logMessage(user+" ACCESSED tool "+toolNameLong)
|
||
363 | if common.checkAck(resp):
|
||
364 | print "ACK" |
||
365 | toolFails[toolName] = 0
|
||
366 | else:
|
||
367 | print "NO ACK!!!!" |
||
368 | logMessage("tool "+toolNameLong+" did not ACK") |
||
369 | toolFails[toolName] += 1
|
||
370 | if toolFails[toolName] > common.MAX_TOOL_FAILS:
|
||
371 | #TODO: send email
|
||
372 | logMessage("WARNING: tool "+toolNameLong+
|
||
373 | " has failed to ACK "+
|
||
374 | str(common.MAX_TOOL_FAILS)+" times in a row.") |
||
375 | |||
376 | 143 | bneuman | else:
|
377 | 230 | bneuman | common.sendNack(2)
|
378 | print "ACCESS DENIED!!" |
||
379 | logMessage(user + " DENIED on tool "+toolNameLong)
|
||
380 | 120 | bneuman | |
381 | 230 | bneuman | clear_id() |
382 | |||
383 | 234 | bneuman | elif resp == common.TT_TIMEOUT:
|
384 | print "TIMEOUT sent from keypad" |
||
385 | continue
|
||
386 | 230 | bneuman | elif resp == None or ord(resp) == 0: #if we get noise, send timeout to reset and sync states |
387 | print "ERROR: got strange byte, timing out" |
||
388 | logMessage("ERROR: got 0 as a byte, sending timeout (cardbox power issue?)")
|
||
389 | 143 | bneuman | else:
|
390 | 230 | bneuman | break
|
391 | except ctrlDException:
|
||
392 | print "got a ctrl-D, exiting cleanly" |
||
393 | finally:
|
||
394 | if not threadDone: |
||
395 | print "stopping id reader thread..." |
||
396 | idt.stop() |