/[anoncvs]/projects/dispense2/sql-edition/servers/VendServer.py


UCC Code Repository

Contents of /projects/dispense2/sql-edition/servers/VendServer.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.102 - (show annotations) (download) (as text)
Sat Aug 24 06:51:35 2013 UTC (7 years, 8 months ago) by tpg
Branch: MAIN
CVS Tags: HEAD
Changes since 1.101: +118 -60 lines
File MIME type: text/x-python
subprocess.Popen, new pin code, disabled users, bugfixes

1 #!/usr/bin/python
2 # vim:ts=4
3
4 USE_DB = 0
5 USE_MIFARE = 1
6
7 import ConfigParser
8 import sys, os, string, re, pwd, signal, math, syslog
9 import logging, logging.handlers
10 from traceback import format_tb
11 if USE_DB: import pg
12 from time import time, sleep, mktime, localtime
13 from subprocess import Popen, PIPE
14 from LATClient import LATClient, LATClientException
15 from SerialClient import SerialClient, SerialClientException
16 from VendingMachine import VendingMachine, VendingException
17 from MessageKeeper import MessageKeeper
18 from HorizScroll import HorizScroll
19 from random import random, seed
20 from Idler import GreetingIdler,TrainIdler,GrayIdler,StringIdler,ClockIdler,FortuneIdler,FileIdler,PipeIdler
21 from SnackConfig import get_snack#, get_snacks
22 import socket
23 from posix import geteuid
24 from LDAPConnector import get_uid,get_uname, set_card_id
25
26 CREDITS="""
27 This vending machine software brought to you by:
28 Bernard Blackham
29 Mark Tearle
30 Nick Bannon
31 Cameron Patrick
32 and a collective of hungry alpacas.
33
34
35
36 For a good time call +61 8 6488 3901
37
38
39
40 """
41
42 PIN_LENGTH = 4
43
44 DOOR = 1
45 SWITCH = 2
46 KEY = 3
47 TICK = 4
48 MIFARE = 5
49
50
51 (
52 STATE_IDLE,
53 STATE_DOOR_OPENING,
54 STATE_DOOR_CLOSING,
55 STATE_GETTING_UID,
56 STATE_GETTING_PIN,
57 STATE_GET_SELECTION,
58 STATE_GRANDFATHER_CLOCK,
59 ) = range(1,8)
60
61 TEXT_SPEED = 0.8
62 IDLE_SPEED = 0.05
63
64 class DispenseDatabaseException(Exception): pass
65
66 class DispenseDatabase:
67 def __init__(self, vending_machine, host, name, user, password):
68 self.vending_machine = vending_machine
69 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
70 self.db.query('LISTEN vend_requests')
71
72 def process_requests(self):
73 logging.debug('database processing')
74 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
75 try:
76 outstanding = self.db.query(query).getresult()
77 except (pg.error,), db_err:
78 raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
79 for (id, slot) in outstanding:
80 (worked, code, string) = self.vending_machine.vend(slot)
81 logging.debug (str((worked, code, string)))
82 if worked:
83 query = 'SELECT vend_success(%s)'%id
84 self.db.query(query).getresult()
85 else:
86 query = 'SELECT vend_failed(%s)'%id
87 self.db.query(query).getresult()
88
89 def handle_events(self):
90 notifier = self.db.getnotify()
91 while notifier is not None:
92 self.process_requests()
93 notify = self.db.getnotify()
94
95 def scroll_options(username, mk, welcome = False):
96 if welcome:
97 # Balance checking
98 acct, unused = Popen(['dispense', 'acct', username], close_fds=True, stdout=PIPE).communicate()
99 # this is fucking appalling
100 balance = acct[acct.find("$")+1:acct.find("(")].strip()
101
102 msg = [(center('WELCOME'), False, TEXT_SPEED),
103 (center(username), False, TEXT_SPEED),
104 (center(balance), False, TEXT_SPEED),]
105 else:
106 msg = []
107 choices = ' '*10+'CHOICES: '
108
109 # Get coke contents
110 cokes = []
111 for i in range(0, 7):
112 args = ('dispense', 'iteminfo', 'coke:%i' % i)
113 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
114 m = re.match("\s*[a-z]+:\d+\s+(\d+)\.(\d\d)\s+([^\n]+)", info)
115 cents = int(m.group(1))*100 + int(m.group(2))
116 cokes.append('%i %i %s' % (i, cents, m.group(3)));
117
118 for c in cokes:
119 c = c.strip()
120 (slot_num, price, slot_name) = c.split(' ', 2)
121 if slot_name == 'dead': continue
122 choices += '%s-(%sc)-%s8 '%(slot_name, price, slot_num)
123
124 # we don't want to print snacks for now since it'll be too large
125 # and there's physical bits of paper in the machine anyway - matt
126 # try:
127 # snacks = get_snacks()
128 # except:
129 # snacks = {}
130 #
131 # for slot, ( name, price ) in snacks.items():
132 # choices += '%s8-%s (%sc) ' % ( slot, name, price )
133
134 choices += '55-DOOR '
135 choices += 'OR ANOTHER SNACK. '
136 choices += '99 TO READ AGAIN. '
137 choices += 'CHOICE? '
138 msg.append((choices, False, None))
139 mk.set_messages(msg)
140
141 _pin_uid = 0
142 _pin_uname = 'root'
143 _pin_pin = '----'
144
145 def _check_pin(uid, pin):
146 global _pin_uid
147 global _pin_uname
148 global _pin_pin
149 print "_check_pin('",uid,"',---)"
150 if uid != _pin_uid:
151 try:
152 info = pwd.getpwuid(uid)
153 except KeyError:
154 logging.info('getting pin for uid %d: user not in password file'%uid)
155 return None
156 if info.pw_dir == None: return False
157 pinfile = os.path.join(info.pw_dir, '.pin')
158 try:
159 s = os.stat(pinfile)
160 except OSError:
161 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
162 return None
163 if s.st_mode & 077:
164 logging.info('getting pin for uid %d: .pin has wrong permissions. Fixing.'%uid)
165 os.chmod(pinfile, 0600)
166 try:
167 f = file(pinfile)
168 except IOError:
169 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
170 return None
171 pinstr = f.readline()
172 f.close()
173 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
174 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
175 return None
176 _pin_uid = uid
177 _pin_pin = pinstr
178 _pin_uname = info.pw_name
179 else:
180 pinstr = _pin_pin
181 if pin == int(pinstr):
182 logging.info("Pin correct for %d",uid)
183 else:
184 logging.info("Pin incorrect for %d",uid)
185 return pin == int(pinstr)
186
187 def acct_is_disabled(name=None):
188 global _pin_uname
189 if name == None:
190 name = _pin_uname
191 acct, unused = Popen(['dispense', 'acct', _pin_uname], close_fds=True, stdout=PIPE).communicate()
192 # this is fucking appalling
193 flags = acct[acct.find("(")+1:acct.find(")")].strip()
194 if 'disabled' in flags:
195 return True
196 if 'internal' in flags:
197 return True
198 return False
199
200 def has_good_pin(uid):
201 return _check_pin(uid, None) != None
202
203 def verify_user_pin(uid, pin, skip_pin_check=False):
204 if skip_pin_check or _check_pin(uid, pin) == True:
205 info = pwd.getpwuid(uid)
206 if skip_pin_check:
207 if acct_is_disabled(info.pw_name):
208 logging.info('refused mifare for disabled acct uid %d (%s)'%(uid,info.pw_name))
209 return '-disabled-'
210 logging.info('accepted mifare for uid %d (%s)'%(uid,info.pw_name))
211 else:
212 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
213 return info.pw_name
214 else:
215 logging.info('refused pin for uid %d'%(uid))
216 return None
217
218
219 def cookie(v):
220 seed(time())
221 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
222 choice = int(random()*len(messages))
223 msg = messages[choice]
224 left = range(len(msg))
225 for i in range(len(msg)):
226 if msg[i] == ' ': left.remove(i)
227 reveal = 1
228 while left:
229 s = ''
230 for i in range(0, len(msg)):
231 if i in left:
232 if reveal == 0:
233 left.remove(i)
234 s += msg[i]
235 else:
236 s += chr(int(random()*26)+ord('A'))
237 reveal += 1
238 reveal %= 17
239 else:
240 s += msg[i]
241 v.display(s)
242
243 def center(str):
244 LEN = 10
245 return ' '*((LEN-len(str))/2)+str
246
247
248
249 idlers = []
250 idler = None
251
252 def setup_idlers(v):
253 global idlers, idler
254 idlers = [
255 GrayIdler(v),
256 StringIdler(v, text="Kill 'em all", repeat=False),
257 GrayIdler(v,one="*",zero="-"),
258 StringIdler(v, text=CREDITS),
259 GrayIdler(v,one="/",zero="\\"),
260 ClockIdler(v),
261 GrayIdler(v,one="X",zero="O"),
262 FileIdler(v, '/usr/share/common-licenses/GPL-2',affinity=2),
263 GrayIdler(v,one="*",zero="-",reorder=1),
264 StringIdler(v, text=str(math.pi) + " "),
265 ClockIdler(v),
266 GrayIdler(v,one="/",zero="\\",reorder=1),
267 StringIdler(v, text=str(math.e) + " "),
268 GrayIdler(v,one="X",zero="O",reorder=1),
269 StringIdler(v, text=" I want some pizza - please call Pizza Hut Shenton Park on +61 8 9381 9979 [now closed? - MSH] - and order as Quinn - I am getting really hungry", repeat=False),
270 PipeIdler(v, "/usr/bin/getent", "passwd"),
271 FortuneIdler(v),
272 ClockIdler(v),
273 StringIdler(v),
274 TrainIdler(v),
275 # "Hello World" in brainfuck
276 StringIdler(v, text=">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++."),
277 ]
278 disabled = [
279 ]
280
281 def reset_idler(v, vstatus, t = None):
282 global idlers, idler
283 idler = GreetingIdler(v, t)
284 vstatus.time_of_next_idlestep = time()+idler.next()
285 vstatus.time_of_next_idler = None
286 vstatus.time_to_autologout = None
287 vstatus.change_state(STATE_IDLE, 1)
288
289 def choose_idler():
290 global idlers, idler
291 iiindex = 0
292 average_affinity = 10 # guessing here...
293
294 if idler and idler.__class__ != GreetingIdler:
295 iiindex = idlers.index(idler)
296
297 iilen = len(idlers)
298
299 move = int(random()*len(idlers)*average_affinity) + 1
300
301 while move >= 0:
302 iiindex += 1
303 iiindex %= iilen
304 idler = idlers[iiindex]
305 move -= idler.affinity()
306
307 idler.reset()
308
309 def idle_step(vstatus):
310 global idler
311 if idler.finished():
312 choose_idler()
313 vstatus.time_of_next_idler = time() + 30
314 nextidle = idler.next()
315 if nextidle is None:
316 nextidle = IDLE_SPEED
317 vstatus.time_of_next_idlestep = time()+nextidle
318
319 class VendState:
320 def __init__(self,v):
321 self.state_table = {}
322 self.state = STATE_IDLE
323 self.counter = 0
324
325 self.mk = MessageKeeper(v)
326 self.cur_user = ''
327 self.cur_pin = ''
328 self.username = ''
329 self.cur_selection = ''
330 self.time_to_autologout = None
331
332 self.last_timeout_refresh = None
333
334 def change_state(self,newstate,newcounter=None):
335 if self.state != newstate:
336 #print "Changing state from: ",
337 #print self.state,
338 #print " to ",
339 #print newstate
340 self.state = newstate
341
342 if newcounter is not None and self.counter != newcounter:
343 #print "Changing counter from: ",
344 #print self.counter,
345 #print " to ",
346 #print newcounter
347 self.counter = newcounter
348
349
350
351 def handle_tick_event(event, params, v, vstatus):
352 # don't care right now.
353 pass
354
355 def handle_switch_event(event, params, v, vstatus):
356 # don't care right now.
357 pass
358
359
360 def do_nothing(state, event, params, v, vstatus):
361 print "doing nothing (s,e,p)", state, " ", event, " ", params
362 pass
363
364 def handle_getting_uid_idle(state, event, params, v, vstatus):
365 # don't care right now.
366 pass
367
368 def handle_getting_pin_idle(state, event, params, v, vstatus):
369 # don't care right now.
370 pass
371
372 def handle_get_selection_idle(state, event, params, v, vstatus):
373 # don't care right now.
374 ###
375 ### State logging out ..
376 if vstatus.time_to_autologout != None:
377 time_left = vstatus.time_to_autologout - time()
378 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
379 vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
380 vstatus.last_timeout_refresh = int(time_left)
381 vstatus.cur_selection = ''
382
383 if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
384 vstatus.time_to_autologout = None
385 vstatus.cur_user = ''
386 vstatus.cur_pin = ''
387 vstatus.cur_selection = ''
388
389 reset_idler(v, vstatus)
390
391 ### State fully logged out ... reset variables
392 if vstatus.time_to_autologout and not vstatus.mk.done():
393 vstatus.time_to_autologout = None
394 if vstatus.cur_user == '' and vstatus.time_to_autologout:
395 vstatus.time_to_autologout = None
396
397 ### State logged in
398 if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
399 # start autologout
400 vstatus.time_to_autologout = time() + 15
401 vstatus.last_timeout_refresh = None
402
403 ## FIXME - this may need to be elsewhere.....
404 # need to check
405 vstatus.mk.update_display()
406
407
408
409 def handle_get_selection_key(state, event, params, v, vstatus):
410 key = params
411 if len(vstatus.cur_selection) == 0:
412 if key == 11:
413 vstatus.cur_pin = ''
414 vstatus.cur_user = ''
415 vstatus.cur_selection = ''
416
417 vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
418 reset_idler(v, vstatus, 2)
419 return
420 vstatus.cur_selection += chr(key + ord('0'))
421 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
422 vstatus.time_to_autologout = None
423 elif len(vstatus.cur_selection) == 1:
424 if key == 11:
425 vstatus.cur_selection = ''
426 vstatus.time_to_autologout = None
427 scroll_options(vstatus.username, vstatus.mk)
428 return
429 else:
430 vstatus.cur_selection += chr(key + ord('0'))
431 if vstatus.cur_user:
432 make_selection(v,vstatus)
433 vstatus.cur_selection = ''
434 vstatus.time_to_autologout = time() + 8
435 vstatus.last_timeout_refresh = None
436 else:
437 # Price check mode.
438 price_check(v,vstatus)
439 vstatus.cur_selection = ''
440 vstatus.time_to_autologout = None
441 vstatus.last_timeout_refresh = None
442
443 def make_selection(v, vstatus):
444 # should use sudo here
445 if vstatus.cur_selection == '55':
446 vstatus.mk.set_message('OPENSESAME')
447 logging.info('dispensing a door for %s'%vstatus.username)
448 if geteuid() == 0:
449 #ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
450 ret = os.system('dispense -u "%s" door'%vstatus.username)
451 else:
452 ret = os.system('dispense door')
453 if ret == 0:
454 logging.info('door opened')
455 vstatus.mk.set_message(center('DOOR OPEN'))
456 else:
457 logging.warning('user %s tried to dispense a bad door'%vstatus.username)
458 vstatus.mk.set_message(center('BAD DOOR'))
459 sleep(1)
460 elif vstatus.cur_selection == '81':
461 cookie(v)
462 elif vstatus.cur_selection == '99':
463 scroll_options(vstatus.username, vstatus.mk)
464 vstatus.cur_selection = ''
465 return
466 elif vstatus.cur_selection[1] == '8':
467 v.display('GOT DRINK?')
468 if ((os.system('dispense -u "%s" coke:%s'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
469 v.display('SEEMS NOT')
470 else:
471 v.display('GOT DRINK!')
472 else:
473 # first see if it's a named slot
474 try:
475 price, shortname, name = get_snack( vstatus.cur_selection )
476 except:
477 price, shortname, name = get_snack( '--' )
478 dollarprice = "$%.2f" % ( price / 100.0 )
479 v.display(vstatus.cur_selection+' - %s'%dollarprice)
480 # exitcode = os.system('dispense -u "%s" give \>snacksales %d "%s"'%(vstatus.username, price, name)) >> 8
481 # exitcode = os.system('dispense -u "%s" give \>sales\:snack %d "%s"'%(vstatus.username, price, name)) >> 8
482 exitcode = os.system('dispense -u "%s" snack:%s'%(vstatus.username, vstatus.cur_selection)) >> 8
483 if (exitcode == 0):
484 # magic dispense syslog service
485 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
486 (worked, code, string) = v.vend(vstatus.cur_selection)
487 if worked:
488 v.display('THANK YOU')
489 else:
490 print "Vend Failed:", code, string
491 v.display('VEND FAIL')
492 elif (exitcode == 5): # RV_BALANCE
493 v.display('NO MONEY?')
494 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
495 v.display('EMPTY SLOT')
496 elif (exitcode == 1): # RV_BADITEM (Dead slot)
497 v.display('EMPTY SLOT')
498 else:
499 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
500 v.display('UNK ERROR')
501 sleep(1)
502
503
504 def price_check(v, vstatus):
505 if vstatus.cur_selection[1] == '8':
506 args = ('dispense', 'iteminfo', 'coke:' + vstatus.cur_selection[0])
507 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
508 dollarprice = re.match("\s*[a-z]+:\d+\s+(\d+\.\d\d)\s+([^\n]+)", info).group(1)
509 else:
510 # first see if it's a named slot
511 try:
512 price, shortname, name = get_snack( vstatus.cur_selection )
513 except:
514 price, shortname, name = get_snack( '--' )
515 dollarprice = "$%.2f" % ( price / 100.0 )
516 v.display(vstatus.cur_selection+' - %s'%dollarprice)
517
518
519 def handle_getting_pin_key(state, event, params, v, vstatus):
520 #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
521 key = params
522 if len(vstatus.cur_pin) < PIN_LENGTH:
523 if key == 11:
524 if vstatus.cur_pin == '':
525 vstatus.cur_user = ''
526 reset_idler(v, vstatus)
527
528 return
529 vstatus.cur_pin = ''
530 vstatus.mk.set_message('PIN: ')
531 return
532 vstatus.cur_pin += chr(key + ord('0'))
533 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
534 if len(vstatus.cur_pin) == PIN_LENGTH:
535 vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
536 if vstatus.username:
537 v.beep(0, False)
538 vstatus.cur_selection = ''
539 vstatus.change_state(STATE_GET_SELECTION)
540 scroll_options(vstatus.username, vstatus.mk, True)
541 return
542 else:
543 v.beep(40, False)
544 vstatus.mk.set_messages(
545 [(center('BAD PIN'), False, 1.0),
546 (center('SORRY'), False, 0.5)])
547 vstatus.cur_user = ''
548 vstatus.cur_pin = ''
549
550 reset_idler(v, vstatus, 2)
551
552 return
553
554
555 def handle_getting_uid_key(state, event, params, v, vstatus):
556 #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
557 key = params
558
559 # complicated key handling here:
560
561 if len(vstatus.cur_user) == 0 and key == 9:
562 vstatus.cur_selection = ''
563 vstatus.time_to_autologout = None
564 vstatus.mk.set_message('PRICECHECK')
565 sleep(0.5)
566 scroll_options('', vstatus.mk)
567 vstatus.change_state(STATE_GET_SELECTION)
568 return
569
570 if len(vstatus.cur_user) <8:
571 if key == 11:
572 vstatus.cur_user = ''
573
574 reset_idler(v, vstatus)
575 return
576 vstatus.cur_user += chr(key + ord('0'))
577 #logging.info('dob: '+vstatus.cur_user)
578 if len(vstatus.cur_user) > 5:
579 vstatus.mk.set_message('>'+vstatus.cur_user)
580 else:
581 vstatus.mk.set_message('UID: '+vstatus.cur_user)
582
583 if len(vstatus.cur_user) == 5:
584 uid = int(vstatus.cur_user)
585
586 if uid == 0:
587 logging.info('user '+vstatus.cur_user+' has a bad PIN')
588 pfalken="""
589 CARRIER DETECTED
590
591 CONNECT 128000
592
593 Welcome to Picklevision Sytems, Sunnyvale, CA
594
595 Greetings Professor Falken.
596
597
598
599
600 Shall we play a game?
601
602
603 Please choose from the following menu:
604
605 1. Tic-Tac-Toe
606 2. Chess
607 3. Checkers
608 4. Backgammon
609 5. Poker
610 6. Toxic and Biochemical Warfare
611 7. Global Thermonuclear War
612
613 7 [ENTER]
614
615 Wouldn't you prefer a nice game of chess?
616
617 """.replace('\n',' ')
618 vstatus.mk.set_messages([(pfalken, False, 10)])
619 vstatus.cur_user = ''
620 vstatus.cur_pin = ''
621
622 reset_idler(v, vstatus, 10)
623
624 return
625
626 if not has_good_pin(uid):
627 logging.info('user '+vstatus.cur_user+' has a bad PIN')
628 vstatus.mk.set_messages(
629 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
630 vstatus.cur_user = ''
631 vstatus.cur_pin = ''
632
633 reset_idler(v, vstatus, 3)
634
635 return
636
637 if acct_is_disabled():
638 logging.info('user '+vstatus.cur_user+' is disabled')
639 vstatus.mk.set_messages(
640 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
641 vstatus.cur_user = ''
642 vstatus.cur_pin = ''
643
644 reset_idler(v, vstatus, 3)
645 return
646
647
648 vstatus.cur_pin = ''
649 vstatus.mk.set_message('PIN: ')
650 logging.info('need pin for user %s'%vstatus.cur_user)
651 vstatus.change_state(STATE_GETTING_PIN)
652 return
653
654
655 def handle_idle_key(state, event, params, v, vstatus):
656 #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
657
658 key = params
659
660 if key == 11:
661 vstatus.cur_user = ''
662 reset_idler(v, vstatus)
663 return
664
665 vstatus.change_state(STATE_GETTING_UID)
666 run_handler(event, key, v, vstatus)
667
668
669 def handle_idle_tick(state, event, params, v, vstatus):
670 ### State idling
671 if vstatus.mk.done():
672 idle_step(vstatus)
673
674 if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
675 vstatus.time_of_next_idler = time() + 30
676 choose_idler()
677
678 ###
679
680 vstatus.mk.update_display()
681
682 vstatus.change_state(STATE_GRANDFATHER_CLOCK)
683 run_handler(event, params, v, vstatus)
684 sleep(0.05)
685
686 def beep_on(when, before=0):
687 start = int(when - before)
688 end = int(when)
689 now = int(time())
690
691 if now >= start and now <= end:
692 return 1
693 return 0
694
695 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
696 ### check for interesting times
697 now = localtime()
698
699 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
700 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
701 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
702 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
703
704 hourfromnow = localtime(time() + 3600)
705
706 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
707 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
708 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
709
710 ## check for X seconds to the hour
711 ## if case, update counter to 2
712 if beep_on(onthehour,15) \
713 or beep_on(halfhour,0) \
714 or beep_on(quarterhour,0) \
715 or beep_on(threequarterhour,0) \
716 or beep_on(fivetothehour,0):
717 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
718 run_handler(event, params, v, vstatus)
719 else:
720 vstatus.change_state(STATE_IDLE)
721
722 def handle_grandfather_tick(state, event, params, v, vstatus):
723 go_idle = 1
724
725 msg = []
726 ### we live in interesting times
727 now = localtime()
728
729 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
730 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
731 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
732 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
733
734 hourfromnow = localtime(time() + 3600)
735
736 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
737 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
738 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
739
740
741 #print "when it fashionable to wear a onion on your hip"
742
743 if beep_on(onthehour,15):
744 go_idle = 0
745 next_hour=((hourfromnow[3] + 11) % 12) + 1
746 if onthehour - time() < next_hour and onthehour - time() > 0:
747 v.beep(0, False)
748
749 t = int(time())
750 if (t % 2) == 0:
751 msg.append(("DING!", False, None))
752 else:
753 msg.append((" DING!", False, None))
754 elif int(onthehour - time()) == 0:
755 v.beep(255, False)
756 msg.append((" BONG!", False, None))
757 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
758 elif beep_on(halfhour,0):
759 go_idle = 0
760 v.beep(0, False)
761 msg.append((" HALFHOUR ", False, 50))
762 elif beep_on(quarterhour,0):
763 go_idle = 0
764 v.beep(0, False)
765 msg.append((" QTR HOUR ", False, 50))
766 elif beep_on(threequarterhour,0):
767 go_idle = 0
768 v.beep(0, False)
769 msg.append((" 3 QTR HR ", False, 50))
770 elif beep_on(fivetothehour,0):
771 go_idle = 0
772 v.beep(0, False)
773 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
774 else:
775 go_idle = 1
776
777 ## check for X seconds to the hour
778
779 if len(msg):
780 vstatus.mk.set_messages(msg)
781 sleep(1)
782
783 vstatus.mk.update_display()
784 ## if no longer case, return to idle
785
786 ## change idler to be clock
787 if go_idle and vstatus.mk.done():
788 vstatus.change_state(STATE_IDLE,1)
789
790 def handle_door_idle(state, event, params, v, vstatus):
791 def twiddle(clock,v,wise = 2):
792 if (clock % 4 == 0):
793 v.display("-FEED ME-")
794 elif (clock % 4 == 1+wise):
795 v.display("\\FEED ME/")
796 elif (clock % 4 == 2):
797 v.display("-FEED ME-")
798 elif (clock % 4 == 3-wise):
799 v.display("/FEED ME\\")
800
801 # don't care right now.
802 now = int(time())
803
804 if ((now % 60 % 2) == 0):
805 twiddle(now, v)
806 else:
807 twiddle(now, v, wise=0)
808
809
810 def handle_door_event(state, event, params, v, vstatus):
811 if params == 0: #door open
812 vstatus.change_state(STATE_DOOR_OPENING)
813 logging.warning("Entering open door mode")
814 v.display("-FEED ME-")
815 #door_open_mode(v);
816 vstatus.cur_user = ''
817 vstatus.cur_pin = ''
818 elif params == 1: #door closed
819 vstatus.change_state(STATE_DOOR_CLOSING)
820 reset_idler(v, vstatus, 3)
821
822 logging.warning('Leaving open door mode')
823 v.display("-YUM YUM!-")
824
825 def handle_mifare_event(state, event, params, v, vstatus):
826 card_id = params
827 # Translate card_id into uid.
828 if card_id == None:
829 return
830
831 try:
832 vstatus.cur_user = get_uid(card_id)
833 logging.info('Mapped card id to uid %s'%vstatus.cur_user)
834 vstatus.username = get_uname(vstatus.cur_user)
835 if acct_is_disabled(vstatus.username):
836 vstatus.username = '-disabled-'
837 except ValueError:
838 vstatus.username = None
839 if vstatus.username == '-disabled-':
840 v.beep(40, False)
841 vstatus.mk.set_messages(
842 [(center('ACCT DISABLED'), False, 1.0),
843 (center('SORRY'), False, 0.5)])
844 vstatus.cur_user = ''
845 vstatus.cur_pin = ''
846 vstatus.username = None
847
848 reset_idler(v, vstatus, 2)
849 return
850 elif vstatus.username:
851 v.beep(0, False)
852 vstatus.cur_selection = ''
853 vstatus.change_state(STATE_GET_SELECTION)
854 scroll_options(vstatus.username, vstatus.mk, True)
855 return
856 else:
857 v.beep(40, False)
858 vstatus.mk.set_messages(
859 [(center('BAD CARD'), False, 1.0),
860 (center('SORRY'), False, 0.5)])
861 vstatus.cur_user = ''
862 vstatus.cur_pin = ''
863
864 reset_idler(v, vstatus, 2)
865 return
866
867 def handle_mifare_add_user_event(state, event, params, v, vstatus):
868 card_id = params
869
870 # Translate card_id into uid.
871 if card_id == None:
872 return
873
874 try:
875 if get_uid(card_id) != None:
876 vstatus.mk.set_messages(
877 [(center('ALREADY'), False, 0.5),
878 (center('ENROLLED'), False, 0.5)])
879
880 # scroll_options(vstatus.username, vstatus.mk)
881 return
882 except ValueError:
883 pass
884
885 logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
886 set_card_id(vstatus.cur_user, card_id)
887 vstatus.mk.set_messages(
888 [(center('CARD'), False, 0.5),
889 (center('ENROLLED'), False, 0.5)])
890
891 # scroll_options(vstatus.username, vstatus.mk)
892
893 def return_to_idle(state,event,params,v,vstatus):
894 reset_idler(v, vstatus)
895
896 def create_state_table(vstatus):
897 vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
898 vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
899 vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
900 vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
901
902 vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
903 vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
904 vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
905 vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
906
907 vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
908 vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
909 vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
910 vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
911
912 vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
913 vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
914 vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
915 vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
916
917 vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
918 vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
919 vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
920 vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
921
922 vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
923 vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
924 vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
925 vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
926
927 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
928 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
929 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
930 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
931 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
932 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
933 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
934
935 def get_state_table_handler(vstatus, state, event, counter):
936 return vstatus.state_table[(state,event,counter)]
937
938 def time_to_next_update(vstatus):
939 idle_update = vstatus.time_of_next_idlestep - time()
940 if not vstatus.mk.done() and vstatus.mk.next_update is not None:
941 mk_update = vstatus.mk.next_update - time()
942 if mk_update < idle_update:
943 idle_update = mk_update
944 return idle_update
945
946 def run_forever(rfh, wfh, options, cf):
947 v = VendingMachine(rfh, wfh, USE_MIFARE)
948 vstatus = VendState(v)
949 create_state_table(vstatus)
950
951 logging.debug('PING is ' + str(v.ping()))
952
953 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
954
955 setup_idlers(v)
956 reset_idler(v, vstatus)
957
958 # This main loop was hideous and the work of the devil.
959 # This has now been fixed (mostly) - mtearle
960 #
961 #
962 # notes for later surgery
963 # (event, counter, ' ')
964 # V
965 # d[ ] = (method)
966 #
967 # ( return state - not currently implemented )
968
969 while True:
970 if USE_DB:
971 try:
972 db.handle_events()
973 except DispenseDatabaseException, e:
974 logging.error('Database error: '+str(e))
975
976 timeout = time_to_next_update(vstatus)
977 e = v.next_event(timeout)
978 (event, params) = e
979
980 run_handler(event, params, v, vstatus)
981
982 # logging.debug('Got event: ' + repr(e))
983
984
985 def run_handler(event, params, v, vstatus):
986 handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
987 if handler:
988 handler(vstatus.state, event, params, v, vstatus)
989
990 def connect_to_vend(options, cf):
991
992 if options.use_lat:
993 logging.info('Connecting to vending machine using LAT')
994 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
995 rfh, wfh = latclient.get_fh()
996 elif options.use_serial:
997 # Open vending machine via serial.
998 logging.info('Connecting to vending machine using serial')
999 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
1000 rfh,wfh = serialclient.get_fh()
1001 else:
1002 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
1003 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
1004 import socket
1005 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
1006 sock.connect((options.host, options.port))
1007 rfh = sock.makefile('r')
1008 wfh = sock.makefile('w')
1009 global USE_MIFARE
1010 USE_MIFARE = 0
1011
1012 return rfh, wfh
1013
1014 def parse_args():
1015 from optparse import OptionParser
1016
1017 op = OptionParser(usage="%prog [OPTION]...")
1018 op.add_option('-f', '--config-file', default='/etc/dispense2/servers.conf', metavar='FILE', dest='config_file', help='use the specified config file instead of /etc/dispense/servers.conf')
1019 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
1020 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
1021 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
1022 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
1023 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
1024 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
1025 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
1026 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
1027 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
1028 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
1029 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
1030 options, args = op.parse_args()
1031
1032 if len(args) != 0:
1033 op.error('extra command line arguments: ' + ' '.join(args))
1034
1035 return options
1036
1037 config_options = {
1038 'DBServer': ('Database', 'Server'),
1039 'DBName': ('Database', 'Name'),
1040 'DBUser': ('VendingMachine', 'DBUser'),
1041 'DBPassword': ('VendingMachine', 'DBPassword'),
1042
1043 'ServiceName': ('VendingMachine', 'ServiceName'),
1044 'ServicePassword': ('VendingMachine', 'Password'),
1045
1046 'ServerName': ('DecServer', 'Name'),
1047 'ConnectPassword': ('DecServer', 'ConnectPassword'),
1048 'PrivPassword': ('DecServer', 'PrivPassword'),
1049 }
1050
1051 class VendConfigFile:
1052 def __init__(self, config_file, options):
1053 try:
1054 cp = ConfigParser.ConfigParser()
1055 cp.read(config_file)
1056
1057 for option in options:
1058 section, name = options[option]
1059 value = cp.get(section, name)
1060 self.__dict__[option] = value
1061
1062 except ConfigParser.Error, e:
1063 raise SystemExit("Error reading config file "+config_file+": " + str(e))
1064
1065 def create_pid_file(name):
1066 try:
1067 pid_file = file(name, 'w')
1068 pid_file.write('%d\n'%os.getpid())
1069 pid_file.close()
1070 except IOError, e:
1071 logging.warning('unable to write to pid file '+name+': '+str(e))
1072
1073 def set_stuff_up():
1074 def do_nothing(signum, stack):
1075 signal.signal(signum, do_nothing)
1076 def stop_server(signum, stack): raise KeyboardInterrupt
1077 signal.signal(signal.SIGHUP, do_nothing)
1078 signal.signal(signal.SIGTERM, stop_server)
1079 signal.signal(signal.SIGINT, stop_server)
1080
1081 options = parse_args()
1082 config_opts = VendConfigFile(options.config_file, config_options)
1083 if options.daemon: become_daemon()
1084 set_up_logging(options)
1085 if options.pid_file != '': create_pid_file(options.pid_file)
1086
1087 return options, config_opts
1088
1089 def clean_up_nicely(options, config_opts):
1090 if options.pid_file != '':
1091 try:
1092 os.unlink(options.pid_file)
1093 logging.debug('Removed pid file '+options.pid_file)
1094 except OSError: pass # if we can't delete it, meh
1095
1096 def set_up_logging(options):
1097 logger = logging.getLogger()
1098
1099 if not options.daemon:
1100 stderr_logger = logging.StreamHandler(sys.stderr)
1101 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1102 logger.addHandler(stderr_logger)
1103
1104 if options.log_file != '':
1105 try:
1106 file_logger = logging.FileHandler(options.log_file)
1107 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1108 logger.addHandler(file_logger)
1109 except IOError, e:
1110 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1111
1112 if options.syslog != None:
1113 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1114 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1115 logger.addHandler(sys_logger)
1116
1117 if options.quiet:
1118 logger.setLevel(logging.WARNING)
1119 elif options.verbose:
1120 logger.setLevel(logging.DEBUG)
1121 else:
1122 logger.setLevel(logging.INFO)
1123
1124 def become_daemon():
1125 dev_null = file('/dev/null')
1126 fd = dev_null.fileno()
1127 os.dup2(fd, 0)
1128 os.dup2(fd, 1)
1129 os.dup2(fd, 2)
1130 try:
1131 if os.fork() != 0:
1132 sys.exit(0)
1133 os.setsid()
1134 except OSError, e:
1135 raise SystemExit('failed to fork: '+str(e))
1136
1137 def do_vend_server(options, config_opts):
1138 while True:
1139 try:
1140 rfh, wfh = connect_to_vend(options, config_opts)
1141 except (SerialClientException, socket.error), e:
1142 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1143 del exc_traceback
1144 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1145 logging.info("Trying again in 5 seconds.")
1146 sleep(5)
1147 continue
1148
1149 # run_forever(rfh, wfh, options, config_opts)
1150
1151 try:
1152 run_forever(rfh, wfh, options, config_opts)
1153 except VendingException:
1154 logging.error("Connection died, trying again...")
1155 logging.info("Trying again in 5 seconds.")
1156 sleep(5)
1157
1158 if __name__ == '__main__':
1159 options, config_opts = set_stuff_up()
1160 while True:
1161 try:
1162 logging.warning('Starting Vend Server')
1163 do_vend_server(options, config_opts)
1164 logging.error('Vend Server finished unexpectedly, restarting')
1165 except KeyboardInterrupt:
1166 logging.info("Killed by signal, cleaning up")
1167 clean_up_nicely(options, config_opts)
1168 logging.warning("Vend Server stopped")
1169 break
1170 except SystemExit:
1171 break
1172 except:
1173 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1174 tb = format_tb(exc_traceback, 20)
1175 del exc_traceback
1176
1177 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1178 logging.critical("Message: " + str(exc_value))
1179 logging.critical("Traceback:")
1180 for event in tb:
1181 for line in event.split('\n'):
1182 logging.critical(' '+line)
1183 logging.critical("This message should be considered a bug in the Vend Server.")
1184 logging.critical("Please report this to someone who can fix it.")
1185 sleep(10)
1186 logging.warning("Trying again anyway (might not help, but hey...)")
1187

Managed by UCC Webmasters ViewVC Help
Powered by ViewVC 1.1.26