8 import sys, os, string, re, pwd, signal, math, syslog
9 import logging, logging.handlers
10 from traceback import format_tb
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
23 from posix import geteuid
24 from LDAPConnector import get_uid,get_uname, set_card_id
25 from OpenDispense import OpenDispense as Dispense
28 This vending machine software brought to you by:
33 and a collective of hungry alpacas.
35 The MIFARE card reader bought to you by:
38 Bug Hunting and hardware maintenance by:
41 For a good time call +61 8 6488 3901
63 STATE_GRANDFATHER_CLOCK,
79 class DispenseDatabaseException(Exception): pass
81 class DispenseDatabase:
82 def __init__(self, vending_machine, host, name, user, password):
83 self.vending_machine = vending_machine
84 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
85 self.db.query('LISTEN vend_requests')
87 def process_requests(self):
88 logging.debug('database processing')
89 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
91 outstanding = self.db.query(query).getresult()
92 except (pg.error,), db_err:
93 raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
94 for (id, slot) in outstanding:
95 (worked, code, string) = self.vending_machine.vend(slot)
96 logging.debug (str((worked, code, string)))
98 query = 'SELECT vend_success(%s)'%id
99 self.db.query(query).getresult()
101 query = 'SELECT vend_failed(%s)'%id
102 self.db.query(query).getresult()
104 def handle_events(self):
105 notifier = self.db.getnotify()
106 while notifier is not None:
107 self.process_requests()
108 notify = self.db.getnotify()
111 def __init__(self,v):
112 self.state_table = {}
113 self.state = STATE_IDLE
116 self.mk = MessageKeeper(v)
120 self.cur_selection = ''
121 self.time_to_autologout = None
123 self.last_timeout_refresh = None
125 def change_state(self,newstate,newcounter=None):
126 if self.state != newstate:
127 #print "Changing state from: ",
131 self.state = newstate
133 if newcounter is not None and self.counter != newcounter:
134 #print "Changing counter from: ",
138 self.counter = newcounter
142 def scroll_options(username, mk, welcome = False):
145 acct, unused = Popen(['dispense', 'acct', username], close_fds=True, stdout=PIPE).communicate()
146 # this is fucking appalling
147 balance = acct[acct.find("$")+1:acct.find("(")].strip()
149 msg = [(center('WELCOME'), False, TEXT_SPEED),
150 (center(username), False, TEXT_SPEED),
151 (center(balance), False, TEXT_SPEED),]
154 choices = ' '*10+'CHOICES: '
158 for i in range(0, 7):
159 args = ('dispense', 'iteminfo', 'coke:%i' % i)
160 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
161 m = re.match("\s*[a-z]+:\d+\s+(\d+)\.(\d\d)\s+([^\n]+)", info)
162 cents = int(m.group(1))*100 + int(m.group(2))
163 cokes.append('%i %i %s' % (i, cents, m.group(3)));
167 (slot_num, price, slot_name) = c.split(' ', 2)
168 if slot_name == 'dead': continue
169 choices += '%s-(%sc)-%s8 '%(slot_name, price, slot_num)
171 # we don't want to print snacks for now since it'll be too large
172 # and there's physical bits of paper in the machine anyway - matt
174 # snacks = get_snacks()
178 # for slot, ( name, price ) in snacks.items():
179 # choices += '%s8-%s (%sc) ' % ( slot, name, price )
181 choices += '55-DOOR '
182 choices += 'OR ANOTHER SNACK. '
183 choices += '99 TO READ AGAIN. '
184 choices += 'CHOICE? '
185 msg.append((choices, False, None))
188 def _check_pin(uid, pin):
192 print "_check_pin('",uid,"',---)"
195 info = pwd.getpwuid(uid)
197 logging.info('getting pin for uid %d: user not in password file'%uid)
199 if info.pw_dir == None: return False
200 pinfile = os.path.join(info.pw_dir, '.pin')
204 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
207 logging.info('getting pin for uid %d: .pin has wrong permissions. Fixing.'%uid)
208 os.chmod(pinfile, 0600)
212 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
214 pinstr = f.readline()
216 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
217 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
221 _pin_uname = info.pw_name
224 if pin == int(pinstr):
225 logging.info("Pin correct for %d",uid)
227 logging.info("Pin incorrect for %d",uid)
228 return pin == int(pinstr)
230 def acct_is_disabled(name=None):
234 acct, unused = Popen(['dispense', 'acct', _pin_uname], close_fds=True, stdout=PIPE).communicate()
235 # this is fucking appalling
236 flags = acct[acct.find("(")+1:acct.find(")")].strip()
237 if 'disabled' in flags:
239 if 'internal' in flags:
243 def has_good_pin(uid):
244 return _check_pin(uid, None) != None
246 def verify_user_pin(uid, pin, skip_pin_check=False):
247 if skip_pin_check or _check_pin(uid, pin) == True:
248 info = pwd.getpwuid(uid)
250 if acct_is_disabled(info.pw_name):
251 logging.info('refused mifare for disabled acct uid %d (%s)'%(uid,info.pw_name))
253 logging.info('accepted mifare for uid %d (%s)'%(uid,info.pw_name))
255 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
258 logging.info('refused pin for uid %d'%(uid))
264 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
265 choice = int(random()*len(messages))
266 msg = messages[choice]
267 left = range(len(msg))
268 for i in range(len(msg)):
269 if msg[i] == ' ': left.remove(i)
273 for i in range(0, len(msg)):
279 s += chr(int(random()*26)+ord('A'))
288 return ' '*((LEN-len(str))/2)+str
295 GrayIdler(v,one="*",zero="-"),
296 GrayIdler(v,one="/",zero="\\"),
297 GrayIdler(v,one="X",zero="O"),
298 GrayIdler(v,one="*",zero="-",reorder=1),
299 GrayIdler(v,one="/",zero="\\",reorder=1),
300 GrayIdler(v,one="X",zero="O",reorder=1),
306 StringIdler(v), # Hello Cruel World
307 StringIdler(v, text="Kill 'em all", repeat=False),
308 StringIdler(v, text=CREDITS),
309 StringIdler(v, text=str(math.pi) + " "),
310 StringIdler(v, text=str(math.e) + " "),
311 StringIdler(v, text=" I want some pizza - please call Broadway Pizza on +61 8 9389 8500 - and order as Quinn - I am getting really hungry", repeat=False),
312 # "Hello World" in brainfuck
313 StringIdler(v, text=">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++."),
317 FileIdler(v, '/usr/share/common-licenses/GPL-2',affinity=2),
319 PipeIdler(v, "/usr/bin/getent", "passwd"),
320 FortuneIdler(v,affinity=20),
325 def reset_idler(v, vstatus, t = None):
327 idler = GreetingIdler(v, t)
328 vstatus.time_of_next_idlestep = time()+idler.next()
329 vstatus.time_of_next_idler = None
330 vstatus.time_to_autologout = None
331 vstatus.change_state(STATE_IDLE, 1)
336 # Implementation of the King Of the Hill algorithm from;
337 # http://eli.thegreenplace.net/2010/01/22/weighted-random-generation-in-python/
339 #def weighted_choice_king(weights):
342 # for i, w in enumerate(weights):
344 # if random.random() * total < w:
352 for choice in idlers:
353 weight = choice.affinity()
355 if random() * total < weight:
363 def idle_step(vstatus):
367 vstatus.time_of_next_idler = time() + 30
368 nextidle = idler.next()
370 nextidle = IDLE_SPEED
371 vstatus.time_of_next_idlestep = time()+nextidle
375 def handle_tick_event(event, params, v, vstatus):
376 # don't care right now.
379 def handle_switch_event(event, params, v, vstatus):
380 # don't care right now.
384 def do_nothing(state, event, params, v, vstatus):
385 print "doing nothing (s,e,p)", state, " ", event, " ", params
388 def handle_getting_uid_idle(state, event, params, v, vstatus):
389 # don't care right now.
392 def handle_getting_pin_idle(state, event, params, v, vstatus):
393 # don't care right now.
396 def handle_get_selection_idle(state, event, params, v, vstatus):
398 # don't care right now.
400 ### State logging out ..
401 if vstatus.time_to_autologout != None:
402 time_left = vstatus.time_to_autologout - time()
403 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
404 vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
405 vstatus.last_timeout_refresh = int(time_left)
406 vstatus.cur_selection = ''
408 if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
409 vstatus.time_to_autologout = None
410 vstatus.cur_user = ''
412 vstatus.cur_selection = ''
414 reset_idler(v, vstatus)
416 ### State fully logged out ... reset variables
417 if vstatus.time_to_autologout and not vstatus.mk.done():
418 vstatus.time_to_autologout = None
419 if vstatus.cur_user == '' and vstatus.time_to_autologout:
420 vstatus.time_to_autologout = None
423 if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
425 vstatus.time_to_autologout = time() + 15
426 vstatus.last_timeout_refresh = None
428 ## FIXME - this may need to be elsewhere.....
430 vstatus.mk.update_display()
434 def handle_get_selection_key(state, event, params, v, vstatus):
437 if len(vstatus.cur_selection) == 0:
440 vstatus.cur_user = ''
441 vstatus.cur_selection = ''
443 vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
444 reset_idler(v, vstatus, 2)
446 vstatus.cur_selection += chr(key + ord('0'))
447 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
448 vstatus.time_to_autologout = None
449 elif len(vstatus.cur_selection) == 1:
451 vstatus.cur_selection = ''
452 vstatus.time_to_autologout = None
453 scroll_options(vstatus.username, vstatus.mk)
456 vstatus.cur_selection += chr(key + ord('0'))
458 make_selection(v,vstatus)
459 vstatus.cur_selection = ''
460 vstatus.time_to_autologout = time() + 8
461 vstatus.last_timeout_refresh = None
464 price_check(v,vstatus)
465 vstatus.cur_selection = ''
466 vstatus.time_to_autologout = None
467 vstatus.last_timeout_refresh = None
469 def make_selection(v, vstatus):
470 # should use sudo here
471 if vstatus.cur_selection == '55':
472 vstatus.mk.set_message('OPENSESAME')
473 logging.info('dispensing a door for %s'%vstatus.username)
475 #ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
476 ret = os.system('dispense -u "%s" door'%vstatus.username)
478 ret = os.system('dispense door')
480 logging.info('door opened')
481 vstatus.mk.set_message(center('DOOR OPEN'))
483 logging.warning('user %s tried to dispense a bad door'%vstatus.username)
484 vstatus.mk.set_message(center('BAD DOOR'))
486 elif vstatus.cur_selection == '81':
488 elif vstatus.cur_selection == '99':
489 scroll_options(vstatus.username, vstatus.mk)
490 vstatus.cur_selection = ''
492 elif vstatus.cur_selection[1] == '8':
493 v.display('GOT DRINK?')
494 if ((os.system('dispense -u "%s" coke:%s'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
495 v.display('SEEMS NOT')
497 v.display('GOT DRINK!')
499 # first see if it's a named slot
501 price, shortname, name = get_snack( vstatus.cur_selection )
503 price, shortname, name = get_snack( '--' )
504 dollarprice = "$%.2f" % ( price / 100.0 )
505 v.display(vstatus.cur_selection+' - %s'%dollarprice)
506 # exitcode = os.system('dispense -u "%s" give \>snacksales %d "%s"'%(vstatus.username, price, name)) >> 8
507 # exitcode = os.system('dispense -u "%s" give \>sales\:snack %d "%s"'%(vstatus.username, price, name)) >> 8
508 exitcode = os.system('dispense -u "%s" snack:%s'%(vstatus.username, vstatus.cur_selection)) >> 8
510 # magic dispense syslog service
511 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
512 (worked, code, string) = v.vend(vstatus.cur_selection)
514 v.display('THANK YOU')
516 print "Vend Failed:", code, string
517 v.display('VEND FAIL')
518 elif (exitcode == 5): # RV_BALANCE
519 v.display('NO MONEY?')
520 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
521 v.display('EMPTY SLOT')
522 elif (exitcode == 1): # RV_BADITEM (Dead slot)
523 v.display('EMPTY SLOT')
525 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
526 v.display('UNK ERROR')
530 def price_check(v, vstatus):
531 if vstatus.cur_selection[1] == '8':
532 args = ('dispense', 'iteminfo', 'coke:' + vstatus.cur_selection[0])
533 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
534 dollarprice = re.match("\s*[a-z]+:\d+\s+(\d+\.\d\d)\s+([^\n]+)", info).group(1)
536 # first see if it's a named slot
538 price, shortname, name = get_snack( vstatus.cur_selection )
540 price, shortname, name = get_snack( '--' )
541 dollarprice = "$%.2f" % ( price / 100.0 )
542 v.display(vstatus.cur_selection+' - %s'%dollarprice)
545 def handle_getting_pin_key(state, event, params, v, vstatus):
546 #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
548 if len(vstatus.cur_pin) < PIN_LENGTH:
550 if vstatus.cur_pin == '':
551 vstatus.cur_user = ''
552 reset_idler(v, vstatus)
556 vstatus.mk.set_message('PIN: ')
558 vstatus.cur_pin += chr(key + ord('0'))
559 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
560 if len(vstatus.cur_pin) == PIN_LENGTH:
561 vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
564 vstatus.cur_selection = ''
565 vstatus.change_state(STATE_GET_SELECTION)
566 scroll_options(vstatus.username, vstatus.mk, True)
570 vstatus.mk.set_messages(
571 [(center('BAD PIN'), False, 1.0),
572 (center('SORRY'), False, 0.5)])
573 vstatus.cur_user = ''
576 reset_idler(v, vstatus, 2)
581 def handle_getting_uid_key(state, event, params, v, vstatus):
582 #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
585 # complicated key handling here:
587 if len(vstatus.cur_user) == 0 and key == 9:
588 vstatus.cur_selection = ''
589 vstatus.time_to_autologout = None
590 vstatus.mk.set_message('PRICECHECK')
592 scroll_options('', vstatus.mk)
593 vstatus.change_state(STATE_GET_SELECTION)
596 if len(vstatus.cur_user) <8:
598 vstatus.cur_user = ''
600 reset_idler(v, vstatus)
602 vstatus.cur_user += chr(key + ord('0'))
603 #logging.info('dob: '+vstatus.cur_user)
604 if len(vstatus.cur_user) > 5:
605 vstatus.mk.set_message('>'+vstatus.cur_user)
607 vstatus.mk.set_message('UID: '+vstatus.cur_user)
609 if len(vstatus.cur_user) == 5:
610 uid = int(vstatus.cur_user)
613 logging.info('user '+vstatus.cur_user+' has a bad PIN')
619 Welcome to Picklevision Sytems, Sunnyvale, CA
621 Greetings Professor Falken.
626 Shall we play a game?
629 Please choose from the following menu:
636 6. Toxic and Biochemical Warfare
637 7. Global Thermonuclear War
641 Wouldn't you prefer a nice game of chess?
643 """.replace('\n',' ')
644 vstatus.mk.set_messages([(pfalken, False, 10)])
645 vstatus.cur_user = ''
648 reset_idler(v, vstatus, 10)
652 if not has_good_pin(uid):
653 logging.info('user '+vstatus.cur_user+' has a bad PIN')
654 vstatus.mk.set_messages(
655 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
656 vstatus.cur_user = ''
659 reset_idler(v, vstatus, 3)
663 if acct_is_disabled():
664 logging.info('user '+vstatus.cur_user+' is disabled')
665 vstatus.mk.set_messages(
666 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
667 vstatus.cur_user = ''
670 reset_idler(v, vstatus, 3)
675 vstatus.mk.set_message('PIN: ')
676 logging.info('need pin for user %s'%vstatus.cur_user)
677 vstatus.change_state(STATE_GETTING_PIN)
681 def handle_idle_key(state, event, params, v, vstatus):
682 #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
687 vstatus.cur_user = ''
688 reset_idler(v, vstatus)
691 vstatus.change_state(STATE_GETTING_UID)
692 run_handler(event, key, v, vstatus)
695 def handle_idle_tick(state, event, params, v, vstatus):
697 if vstatus.mk.done():
700 if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
701 vstatus.time_of_next_idler = time() + 30
706 vstatus.mk.update_display()
708 vstatus.change_state(STATE_GRANDFATHER_CLOCK)
709 run_handler(event, params, v, vstatus)
712 def beep_on(when, before=0):
713 start = int(when - before)
717 if now >= start and now <= end:
721 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
722 ### check for interesting times
725 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
726 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
727 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
728 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
730 hourfromnow = localtime(time() + 3600)
732 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
733 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
734 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
736 ## check for X seconds to the hour
737 ## if case, update counter to 2
738 if beep_on(onthehour,15) \
739 or beep_on(halfhour,0) \
740 or beep_on(quarterhour,0) \
741 or beep_on(threequarterhour,0) \
742 or beep_on(fivetothehour,0):
743 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
744 run_handler(event, params, v, vstatus)
746 vstatus.change_state(STATE_IDLE)
748 def handle_grandfather_tick(state, event, params, v, vstatus):
752 ### we live in interesting times
755 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
756 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
757 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
758 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
760 hourfromnow = localtime(time() + 3600)
762 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
763 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
764 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
767 #print "when it fashionable to wear a onion on your hip"
769 if beep_on(onthehour,15):
771 next_hour=((hourfromnow[3] + 11) % 12) + 1
772 if onthehour - time() < next_hour and onthehour - time() > 0:
777 msg.append(("DING!", False, None))
779 msg.append((" DING!", False, None))
780 elif int(onthehour - time()) == 0:
782 msg.append((" BONG!", False, None))
783 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
784 elif beep_on(halfhour,0):
787 msg.append((" HALFHOUR ", False, 50))
788 elif beep_on(quarterhour,0):
791 msg.append((" QTR HOUR ", False, 50))
792 elif beep_on(threequarterhour,0):
795 msg.append((" 3 QTR HR ", False, 50))
796 elif beep_on(fivetothehour,0):
799 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
803 ## check for X seconds to the hour
806 vstatus.mk.set_messages(msg)
809 vstatus.mk.update_display()
810 ## if no longer case, return to idle
812 ## change idler to be clock
813 if go_idle and vstatus.mk.done():
814 vstatus.change_state(STATE_IDLE,1)
816 def handle_door_idle(state, event, params, v, vstatus):
817 def twiddle(clock,v,wise = 2):
819 v.display("-FEED ME-")
820 elif (clock % 4 == 1+wise):
821 v.display("\\FEED ME/")
822 elif (clock % 4 == 2):
823 v.display("-FEED ME-")
824 elif (clock % 4 == 3-wise):
825 v.display("/FEED ME\\")
827 # don't care right now.
830 if ((now % 60 % 2) == 0):
833 twiddle(now, v, wise=0)
836 def handle_door_event(state, event, params, v, vstatus):
837 if params == 0: #door open
838 vstatus.change_state(STATE_DOOR_OPENING)
839 logging.warning("Entering open door mode")
840 v.display("-FEED ME-")
842 vstatus.cur_user = ''
844 elif params == 1: #door closed
845 vstatus.change_state(STATE_DOOR_CLOSING)
846 reset_idler(v, vstatus, 3)
848 logging.warning('Leaving open door mode')
849 v.display("-YUM YUM!-")
852 def handle_mifare_event(state, event, params, v, vstatus):
855 # Translate card_id into uid.
856 if card_id == None or card_id == _last_card_id:
859 _last_card_id = card_id
862 vstatus.cur_user = get_uid(card_id)
863 logging.info('Mapped card id to uid %s'%vstatus.cur_user)
864 vstatus.username = get_uname(vstatus.cur_user)
865 if acct_is_disabled(vstatus.username):
866 vstatus.username = '-disabled-'
868 vstatus.username = None
869 if vstatus.username == '-disabled-':
871 vstatus.mk.set_messages(
872 [(center('ACCT DISABLED'), False, 1.0),
873 (center('SORRY'), False, 0.5)])
874 vstatus.cur_user = ''
876 vstatus.username = None
878 reset_idler(v, vstatus, 2)
880 elif vstatus.username:
882 vstatus.cur_selection = ''
883 vstatus.change_state(STATE_GET_SELECTION)
884 scroll_options(vstatus.username, vstatus.mk, True)
888 vstatus.mk.set_messages(
889 [(center('BAD CARD'), False, 1.0),
890 (center('SORRY'), False, 0.5)])
891 vstatus.cur_user = ''
895 reset_idler(v, vstatus, 2)
898 def handle_mifare_add_user_event(state, event, params, v, vstatus):
902 # Translate card_id into uid.
903 if card_id == None or card_id == _last_card_id:
906 _last_card_id = card_id
909 if get_uid(card_id) != None:
910 vstatus.mk.set_messages(
911 [(center('ALREADY'), False, 0.5),
912 (center('ENROLLED'), False, 0.5)])
914 # scroll_options(vstatus.username, vstatus.mk)
919 logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
920 set_card_id(vstatus.cur_user, card_id)
921 vstatus.mk.set_messages(
922 [(center('CARD'), False, 0.5),
923 (center('ENROLLED'), False, 0.5)])
925 # scroll_options(vstatus.username, vstatus.mk)
927 def return_to_idle(state,event,params,v,vstatus):
928 reset_idler(v, vstatus)
930 def create_state_table(vstatus):
931 vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
932 vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
933 vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
934 vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
936 vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
937 vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
938 vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
939 vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
941 vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
942 vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
943 vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
944 vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
946 vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
947 vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = handle_door_event
948 vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
949 vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
951 vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
952 vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = handle_door_event
953 vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
954 vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
956 vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
957 vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = handle_door_event
958 vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
959 vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
961 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
962 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
963 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = handle_door_event
964 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = handle_door_event
965 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
966 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
967 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
969 def get_state_table_handler(vstatus, state, event, counter):
970 return vstatus.state_table[(state,event,counter)]
972 def time_to_next_update(vstatus):
973 idle_update = vstatus.time_of_next_idlestep - time()
974 if not vstatus.mk.done() and vstatus.mk.next_update is not None:
975 mk_update = vstatus.mk.next_update - time()
976 if mk_update < idle_update:
977 idle_update = mk_update
980 def run_forever(rfh, wfh, options, cf):
981 v = VendingMachine(rfh, wfh, USE_MIFARE)
982 vstatus = VendState(v)
983 create_state_table(vstatus)
985 logging.debug('PING is ' + str(v.ping()))
987 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
990 reset_idler(v, vstatus)
992 # This main loop was hideous and the work of the devil.
993 # This has now been fixed (mostly) - mtearle
996 # notes for later surgery
997 # (event, counter, ' ')
1001 # ( return state - not currently implemented )
1007 except DispenseDatabaseException, e:
1008 logging.error('Database error: '+str(e))
1010 timeout = time_to_next_update(vstatus)
1011 e = v.next_event(timeout)
1014 run_handler(event, params, v, vstatus)
1016 # logging.debug('Got event: ' + repr(e))
1019 def run_handler(event, params, v, vstatus):
1020 handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
1022 handler(vstatus.state, event, params, v, vstatus)
1024 def connect_to_vend(options, cf):
1027 logging.info('Connecting to vending machine using LAT')
1028 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
1029 rfh, wfh = latclient.get_fh()
1030 elif options.use_serial:
1031 # Open vending machine via serial.
1032 logging.info('Connecting to vending machine using serial')
1033 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
1034 rfh,wfh = serialclient.get_fh()
1036 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
1037 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
1039 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
1040 sock.connect((options.host, options.port))
1041 rfh = sock.makefile('r')
1042 wfh = sock.makefile('w')
1049 from optparse import OptionParser
1051 op = OptionParser(usage="%prog [OPTION]...")
1052 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')
1053 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
1054 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
1055 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
1056 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
1057 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
1058 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
1059 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
1060 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
1061 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
1062 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
1063 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
1064 options, args = op.parse_args()
1067 op.error('extra command line arguments: ' + ' '.join(args))
1072 'DBServer': ('Database', 'Server'),
1073 'DBName': ('Database', 'Name'),
1074 'DBUser': ('VendingMachine', 'DBUser'),
1075 'DBPassword': ('VendingMachine', 'DBPassword'),
1077 'ServiceName': ('VendingMachine', 'ServiceName'),
1078 'ServicePassword': ('VendingMachine', 'Password'),
1080 'ServerName': ('DecServer', 'Name'),
1081 'ConnectPassword': ('DecServer', 'ConnectPassword'),
1082 'PrivPassword': ('DecServer', 'PrivPassword'),
1085 class VendConfigFile:
1086 def __init__(self, config_file, options):
1088 cp = ConfigParser.ConfigParser()
1089 cp.read(config_file)
1091 for option in options:
1092 section, name = options[option]
1093 value = cp.get(section, name)
1094 self.__dict__[option] = value
1096 except ConfigParser.Error, e:
1097 raise SystemExit("Error reading config file "+config_file+": " + str(e))
1099 def create_pid_file(name):
1101 pid_file = file(name, 'w')
1102 pid_file.write('%d\n'%os.getpid())
1105 logging.warning('unable to write to pid file '+name+': '+str(e))
1108 def do_nothing(signum, stack):
1109 signal.signal(signum, do_nothing)
1110 def stop_server(signum, stack): raise KeyboardInterrupt
1111 signal.signal(signal.SIGHUP, do_nothing)
1112 signal.signal(signal.SIGTERM, stop_server)
1113 signal.signal(signal.SIGINT, stop_server)
1115 options = parse_args()
1116 config_opts = VendConfigFile(options.config_file, config_options)
1117 if options.daemon: become_daemon()
1118 set_up_logging(options)
1119 if options.pid_file != '': create_pid_file(options.pid_file)
1121 return options, config_opts
1123 def clean_up_nicely(options, config_opts):
1124 if options.pid_file != '':
1126 os.unlink(options.pid_file)
1127 logging.debug('Removed pid file '+options.pid_file)
1128 except OSError: pass # if we can't delete it, meh
1130 def set_up_logging(options):
1131 logger = logging.getLogger()
1133 if not options.daemon:
1134 stderr_logger = logging.StreamHandler(sys.stderr)
1135 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1136 logger.addHandler(stderr_logger)
1138 if options.log_file != '':
1140 file_logger = logging.FileHandler(options.log_file)
1141 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1142 logger.addHandler(file_logger)
1144 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1146 if options.syslog != None:
1147 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1148 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1149 logger.addHandler(sys_logger)
1152 logger.setLevel(logging.WARNING)
1153 elif options.verbose:
1154 logger.setLevel(logging.DEBUG)
1156 logger.setLevel(logging.INFO)
1158 def become_daemon():
1159 dev_null = file('/dev/null')
1160 fd = dev_null.fileno()
1169 raise SystemExit('failed to fork: '+str(e))
1171 def do_vend_server(options, config_opts):
1174 rfh, wfh = connect_to_vend(options, config_opts)
1175 except (SerialClientException, socket.error), e:
1176 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1178 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1179 logging.info("Trying again in 5 seconds.")
1183 # run_forever(rfh, wfh, options, config_opts)
1186 run_forever(rfh, wfh, options, config_opts)
1187 except VendingException:
1188 logging.error("Connection died, trying again...")
1189 logging.info("Trying again in 5 seconds.")
1193 def main(argv=None):
1194 options, config_opts = set_stuff_up()
1197 logging.warning('Starting Vend Server')
1198 do_vend_server(options, config_opts)
1199 logging.error('Vend Server finished unexpectedly, restarting')
1200 except KeyboardInterrupt:
1201 logging.info("Killed by signal, cleaning up")
1202 clean_up_nicely(options, config_opts)
1203 logging.warning("Vend Server stopped")
1208 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1209 tb = format_tb(exc_traceback, 20)
1212 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1213 logging.critical("Message: " + str(exc_value))
1214 logging.critical("Traceback:")
1216 for line in event.split('\n'):
1217 logging.critical(' '+line)
1218 logging.critical("This message should be considered a bug in the Vend Server.")
1219 logging.critical("Please report this to someone who can fix it.")
1221 logging.warning("Trying again anyway (might not help, but hey...)")
1223 if __name__ == '__main__':