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
27 This vending machine software brought to you by:
32 and a collective of hungry alpacas.
36 For a good time call +61 8 6488 3901
58 STATE_GRANDFATHER_CLOCK,
64 class DispenseDatabaseException(Exception): pass
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')
72 def process_requests(self):
73 logging.debug('database processing')
74 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
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)))
83 query = 'SELECT vend_success(%s)'%id
84 self.db.query(query).getresult()
86 query = 'SELECT vend_failed(%s)'%id
87 self.db.query(query).getresult()
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()
95 def scroll_options(username, mk, welcome = False):
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()
102 msg = [(center('WELCOME'), False, TEXT_SPEED),
103 (center(username), False, TEXT_SPEED),
104 (center(balance), False, TEXT_SPEED),]
107 choices = ' '*10+'CHOICES: '
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)));
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)
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
127 # snacks = get_snacks()
131 # for slot, ( name, price ) in snacks.items():
132 # choices += '%s8-%s (%sc) ' % ( slot, name, price )
134 choices += '55-DOOR '
135 choices += 'OR ANOTHER SNACK. '
136 choices += '99 TO READ AGAIN. '
137 choices += 'CHOICE? '
138 msg.append((choices, False, None))
145 def _check_pin(uid, pin):
149 print "_check_pin('",uid,"',---)"
152 info = pwd.getpwuid(uid)
154 logging.info('getting pin for uid %d: user not in password file'%uid)
156 if info.pw_dir == None: return False
157 pinfile = os.path.join(info.pw_dir, '.pin')
161 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
164 logging.info('getting pin for uid %d: .pin has wrong permissions. Fixing.'%uid)
165 os.chmod(pinfile, 0600)
169 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
171 pinstr = f.readline()
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)))
178 _pin_uname = info.pw_name
181 if pin == int(pinstr):
182 logging.info("Pin correct for %d",uid)
184 logging.info("Pin incorrect for %d",uid)
185 return pin == int(pinstr)
187 def acct_is_disabled(name=None):
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:
196 if 'internal' in flags:
200 def has_good_pin(uid):
201 return _check_pin(uid, None) != None
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)
207 if acct_is_disabled(info.pw_name):
208 logging.info('refused mifare for disabled acct uid %d (%s)'%(uid,info.pw_name))
210 logging.info('accepted mifare for uid %d (%s)'%(uid,info.pw_name))
212 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
215 logging.info('refused pin for uid %d'%(uid))
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)
230 for i in range(0, len(msg)):
236 s += chr(int(random()*26)+ord('A'))
245 return ' '*((LEN-len(str))/2)+str
256 StringIdler(v, text="Kill 'em all", repeat=False),
257 GrayIdler(v,one="*",zero="-"),
258 StringIdler(v, text=CREDITS),
259 GrayIdler(v,one="/",zero="\\"),
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) + " "),
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 Broadway Pizza on +61 8 9389 8500 - and order as Quinn - I am getting really hungry", repeat=False),
270 PipeIdler(v, "/usr/bin/getent", "passwd"),
275 # "Hello World" in brainfuck
276 StringIdler(v, text=">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++."),
281 def reset_idler(v, vstatus, t = None):
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)
292 # Implementation of the King Of the Hill algorithm from;
293 # http://eli.thegreenplace.net/2010/01/22/weighted-random-generation-in-python/
295 #def weighted_choice_king(weights):
298 # for i, w in enumerate(weights):
300 # if random.random() * total < w:
308 for choice in idlers:
309 weight = choice.affinity()
311 if random() * total < weight:
319 def idle_step(vstatus):
323 vstatus.time_of_next_idler = time() + 30
324 nextidle = idler.next()
326 nextidle = IDLE_SPEED
327 vstatus.time_of_next_idlestep = time()+nextidle
330 def __init__(self,v):
331 self.state_table = {}
332 self.state = STATE_IDLE
335 self.mk = MessageKeeper(v)
339 self.cur_selection = ''
340 self.time_to_autologout = None
342 self.last_timeout_refresh = None
344 def change_state(self,newstate,newcounter=None):
345 if self.state != newstate:
346 #print "Changing state from: ",
350 self.state = newstate
352 if newcounter is not None and self.counter != newcounter:
353 #print "Changing counter from: ",
357 self.counter = newcounter
361 def handle_tick_event(event, params, v, vstatus):
362 # don't care right now.
365 def handle_switch_event(event, params, v, vstatus):
366 # don't care right now.
370 def do_nothing(state, event, params, v, vstatus):
371 print "doing nothing (s,e,p)", state, " ", event, " ", params
374 def handle_getting_uid_idle(state, event, params, v, vstatus):
375 # don't care right now.
378 def handle_getting_pin_idle(state, event, params, v, vstatus):
379 # don't care right now.
382 def handle_get_selection_idle(state, event, params, v, vstatus):
383 # don't care right now.
385 ### State logging out ..
386 if vstatus.time_to_autologout != None:
387 time_left = vstatus.time_to_autologout - time()
388 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
389 vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
390 vstatus.last_timeout_refresh = int(time_left)
391 vstatus.cur_selection = ''
393 if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
394 vstatus.time_to_autologout = None
395 vstatus.cur_user = ''
397 vstatus.cur_selection = ''
399 reset_idler(v, vstatus)
401 ### State fully logged out ... reset variables
402 if vstatus.time_to_autologout and not vstatus.mk.done():
403 vstatus.time_to_autologout = None
404 if vstatus.cur_user == '' and vstatus.time_to_autologout:
405 vstatus.time_to_autologout = None
408 if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
410 vstatus.time_to_autologout = time() + 15
411 vstatus.last_timeout_refresh = None
413 ## FIXME - this may need to be elsewhere.....
415 vstatus.mk.update_display()
419 def handle_get_selection_key(state, event, params, v, vstatus):
421 if len(vstatus.cur_selection) == 0:
424 vstatus.cur_user = ''
425 vstatus.cur_selection = ''
427 vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
428 reset_idler(v, vstatus, 2)
430 vstatus.cur_selection += chr(key + ord('0'))
431 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
432 vstatus.time_to_autologout = None
433 elif len(vstatus.cur_selection) == 1:
435 vstatus.cur_selection = ''
436 vstatus.time_to_autologout = None
437 scroll_options(vstatus.username, vstatus.mk)
440 vstatus.cur_selection += chr(key + ord('0'))
442 make_selection(v,vstatus)
443 vstatus.cur_selection = ''
444 vstatus.time_to_autologout = time() + 8
445 vstatus.last_timeout_refresh = None
448 price_check(v,vstatus)
449 vstatus.cur_selection = ''
450 vstatus.time_to_autologout = None
451 vstatus.last_timeout_refresh = None
453 def make_selection(v, vstatus):
454 # should use sudo here
455 if vstatus.cur_selection == '55':
456 vstatus.mk.set_message('OPENSESAME')
457 logging.info('dispensing a door for %s'%vstatus.username)
459 #ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
460 ret = os.system('dispense -u "%s" door'%vstatus.username)
462 ret = os.system('dispense door')
464 logging.info('door opened')
465 vstatus.mk.set_message(center('DOOR OPEN'))
467 logging.warning('user %s tried to dispense a bad door'%vstatus.username)
468 vstatus.mk.set_message(center('BAD DOOR'))
470 elif vstatus.cur_selection == '81':
472 elif vstatus.cur_selection == '99':
473 scroll_options(vstatus.username, vstatus.mk)
474 vstatus.cur_selection = ''
476 elif vstatus.cur_selection[1] == '8':
477 v.display('GOT DRINK?')
478 if ((os.system('dispense -u "%s" coke:%s'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
479 v.display('SEEMS NOT')
481 v.display('GOT DRINK!')
483 # first see if it's a named slot
485 price, shortname, name = get_snack( vstatus.cur_selection )
487 price, shortname, name = get_snack( '--' )
488 dollarprice = "$%.2f" % ( price / 100.0 )
489 v.display(vstatus.cur_selection+' - %s'%dollarprice)
490 # exitcode = os.system('dispense -u "%s" give \>snacksales %d "%s"'%(vstatus.username, price, name)) >> 8
491 # exitcode = os.system('dispense -u "%s" give \>sales\:snack %d "%s"'%(vstatus.username, price, name)) >> 8
492 exitcode = os.system('dispense -u "%s" snack:%s'%(vstatus.username, vstatus.cur_selection)) >> 8
494 # magic dispense syslog service
495 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
496 (worked, code, string) = v.vend(vstatus.cur_selection)
498 v.display('THANK YOU')
500 print "Vend Failed:", code, string
501 v.display('VEND FAIL')
502 elif (exitcode == 5): # RV_BALANCE
503 v.display('NO MONEY?')
504 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
505 v.display('EMPTY SLOT')
506 elif (exitcode == 1): # RV_BADITEM (Dead slot)
507 v.display('EMPTY SLOT')
509 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
510 v.display('UNK ERROR')
514 def price_check(v, vstatus):
515 if vstatus.cur_selection[1] == '8':
516 args = ('dispense', 'iteminfo', 'coke:' + vstatus.cur_selection[0])
517 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
518 dollarprice = re.match("\s*[a-z]+:\d+\s+(\d+\.\d\d)\s+([^\n]+)", info).group(1)
520 # first see if it's a named slot
522 price, shortname, name = get_snack( vstatus.cur_selection )
524 price, shortname, name = get_snack( '--' )
525 dollarprice = "$%.2f" % ( price / 100.0 )
526 v.display(vstatus.cur_selection+' - %s'%dollarprice)
529 def handle_getting_pin_key(state, event, params, v, vstatus):
530 #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
532 if len(vstatus.cur_pin) < PIN_LENGTH:
534 if vstatus.cur_pin == '':
535 vstatus.cur_user = ''
536 reset_idler(v, vstatus)
540 vstatus.mk.set_message('PIN: ')
542 vstatus.cur_pin += chr(key + ord('0'))
543 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
544 if len(vstatus.cur_pin) == PIN_LENGTH:
545 vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
548 vstatus.cur_selection = ''
549 vstatus.change_state(STATE_GET_SELECTION)
550 scroll_options(vstatus.username, vstatus.mk, True)
554 vstatus.mk.set_messages(
555 [(center('BAD PIN'), False, 1.0),
556 (center('SORRY'), False, 0.5)])
557 vstatus.cur_user = ''
560 reset_idler(v, vstatus, 2)
565 def handle_getting_uid_key(state, event, params, v, vstatus):
566 #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
569 # complicated key handling here:
571 if len(vstatus.cur_user) == 0 and key == 9:
572 vstatus.cur_selection = ''
573 vstatus.time_to_autologout = None
574 vstatus.mk.set_message('PRICECHECK')
576 scroll_options('', vstatus.mk)
577 vstatus.change_state(STATE_GET_SELECTION)
580 if len(vstatus.cur_user) <8:
582 vstatus.cur_user = ''
584 reset_idler(v, vstatus)
586 vstatus.cur_user += chr(key + ord('0'))
587 #logging.info('dob: '+vstatus.cur_user)
588 if len(vstatus.cur_user) > 5:
589 vstatus.mk.set_message('>'+vstatus.cur_user)
591 vstatus.mk.set_message('UID: '+vstatus.cur_user)
593 if len(vstatus.cur_user) == 5:
594 uid = int(vstatus.cur_user)
597 logging.info('user '+vstatus.cur_user+' has a bad PIN')
603 Welcome to Picklevision Sytems, Sunnyvale, CA
605 Greetings Professor Falken.
610 Shall we play a game?
613 Please choose from the following menu:
620 6. Toxic and Biochemical Warfare
621 7. Global Thermonuclear War
625 Wouldn't you prefer a nice game of chess?
627 """.replace('\n',' ')
628 vstatus.mk.set_messages([(pfalken, False, 10)])
629 vstatus.cur_user = ''
632 reset_idler(v, vstatus, 10)
636 if not has_good_pin(uid):
637 logging.info('user '+vstatus.cur_user+' has a bad PIN')
638 vstatus.mk.set_messages(
639 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
640 vstatus.cur_user = ''
643 reset_idler(v, vstatus, 3)
647 if acct_is_disabled():
648 logging.info('user '+vstatus.cur_user+' is disabled')
649 vstatus.mk.set_messages(
650 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
651 vstatus.cur_user = ''
654 reset_idler(v, vstatus, 3)
659 vstatus.mk.set_message('PIN: ')
660 logging.info('need pin for user %s'%vstatus.cur_user)
661 vstatus.change_state(STATE_GETTING_PIN)
665 def handle_idle_key(state, event, params, v, vstatus):
666 #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
671 vstatus.cur_user = ''
672 reset_idler(v, vstatus)
675 vstatus.change_state(STATE_GETTING_UID)
676 run_handler(event, key, v, vstatus)
679 def handle_idle_tick(state, event, params, v, vstatus):
681 if vstatus.mk.done():
684 if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
685 vstatus.time_of_next_idler = time() + 30
690 vstatus.mk.update_display()
692 vstatus.change_state(STATE_GRANDFATHER_CLOCK)
693 run_handler(event, params, v, vstatus)
696 def beep_on(when, before=0):
697 start = int(when - before)
701 if now >= start and now <= end:
705 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
706 ### check for interesting times
709 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
710 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
711 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
712 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
714 hourfromnow = localtime(time() + 3600)
716 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
717 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
718 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
720 ## check for X seconds to the hour
721 ## if case, update counter to 2
722 if beep_on(onthehour,15) \
723 or beep_on(halfhour,0) \
724 or beep_on(quarterhour,0) \
725 or beep_on(threequarterhour,0) \
726 or beep_on(fivetothehour,0):
727 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
728 run_handler(event, params, v, vstatus)
730 vstatus.change_state(STATE_IDLE)
732 def handle_grandfather_tick(state, event, params, v, vstatus):
736 ### we live in interesting times
739 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
740 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
741 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
742 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
744 hourfromnow = localtime(time() + 3600)
746 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
747 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
748 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
751 #print "when it fashionable to wear a onion on your hip"
753 if beep_on(onthehour,15):
755 next_hour=((hourfromnow[3] + 11) % 12) + 1
756 if onthehour - time() < next_hour and onthehour - time() > 0:
761 msg.append(("DING!", False, None))
763 msg.append((" DING!", False, None))
764 elif int(onthehour - time()) == 0:
766 msg.append((" BONG!", False, None))
767 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
768 elif beep_on(halfhour,0):
771 msg.append((" HALFHOUR ", False, 50))
772 elif beep_on(quarterhour,0):
775 msg.append((" QTR HOUR ", False, 50))
776 elif beep_on(threequarterhour,0):
779 msg.append((" 3 QTR HR ", False, 50))
780 elif beep_on(fivetothehour,0):
783 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
787 ## check for X seconds to the hour
790 vstatus.mk.set_messages(msg)
793 vstatus.mk.update_display()
794 ## if no longer case, return to idle
796 ## change idler to be clock
797 if go_idle and vstatus.mk.done():
798 vstatus.change_state(STATE_IDLE,1)
800 def handle_door_idle(state, event, params, v, vstatus):
801 def twiddle(clock,v,wise = 2):
803 v.display("-FEED ME-")
804 elif (clock % 4 == 1+wise):
805 v.display("\\FEED ME/")
806 elif (clock % 4 == 2):
807 v.display("-FEED ME-")
808 elif (clock % 4 == 3-wise):
809 v.display("/FEED ME\\")
811 # don't care right now.
814 if ((now % 60 % 2) == 0):
817 twiddle(now, v, wise=0)
820 def handle_door_event(state, event, params, v, vstatus):
821 if params == 0: #door open
822 vstatus.change_state(STATE_DOOR_OPENING)
823 logging.warning("Entering open door mode")
824 v.display("-FEED ME-")
826 vstatus.cur_user = ''
828 elif params == 1: #door closed
829 vstatus.change_state(STATE_DOOR_CLOSING)
830 reset_idler(v, vstatus, 3)
832 logging.warning('Leaving open door mode')
833 v.display("-YUM YUM!-")
835 def handle_mifare_event(state, event, params, v, vstatus):
837 # Translate card_id into uid.
842 vstatus.cur_user = get_uid(card_id)
843 logging.info('Mapped card id to uid %s'%vstatus.cur_user)
844 vstatus.username = get_uname(vstatus.cur_user)
845 if acct_is_disabled(vstatus.username):
846 vstatus.username = '-disabled-'
848 vstatus.username = None
849 if vstatus.username == '-disabled-':
851 vstatus.mk.set_messages(
852 [(center('ACCT DISABLED'), False, 1.0),
853 (center('SORRY'), False, 0.5)])
854 vstatus.cur_user = ''
856 vstatus.username = None
858 reset_idler(v, vstatus, 2)
860 elif vstatus.username:
862 vstatus.cur_selection = ''
863 vstatus.change_state(STATE_GET_SELECTION)
864 scroll_options(vstatus.username, vstatus.mk, True)
868 vstatus.mk.set_messages(
869 [(center('BAD CARD'), False, 1.0),
870 (center('SORRY'), False, 0.5)])
871 vstatus.cur_user = ''
874 reset_idler(v, vstatus, 2)
877 def handle_mifare_add_user_event(state, event, params, v, vstatus):
880 # Translate card_id into uid.
885 if get_uid(card_id) != None:
886 vstatus.mk.set_messages(
887 [(center('ALREADY'), False, 0.5),
888 (center('ENROLLED'), False, 0.5)])
890 # scroll_options(vstatus.username, vstatus.mk)
895 logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
896 set_card_id(vstatus.cur_user, card_id)
897 vstatus.mk.set_messages(
898 [(center('CARD'), False, 0.5),
899 (center('ENROLLED'), False, 0.5)])
901 # scroll_options(vstatus.username, vstatus.mk)
903 def return_to_idle(state,event,params,v,vstatus):
904 reset_idler(v, vstatus)
906 def create_state_table(vstatus):
907 vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
908 vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
909 vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
910 vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
912 vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
913 vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
914 vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
915 vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
917 vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
918 vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
919 vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
920 vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
922 vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
923 vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
924 vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
925 vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
927 vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
928 vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
929 vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
930 vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
932 vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
933 vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
934 vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
935 vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
937 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
938 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
939 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
940 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
941 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
942 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
943 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
945 def get_state_table_handler(vstatus, state, event, counter):
946 return vstatus.state_table[(state,event,counter)]
948 def time_to_next_update(vstatus):
949 idle_update = vstatus.time_of_next_idlestep - time()
950 if not vstatus.mk.done() and vstatus.mk.next_update is not None:
951 mk_update = vstatus.mk.next_update - time()
952 if mk_update < idle_update:
953 idle_update = mk_update
956 def run_forever(rfh, wfh, options, cf):
957 v = VendingMachine(rfh, wfh, USE_MIFARE)
958 vstatus = VendState(v)
959 create_state_table(vstatus)
961 logging.debug('PING is ' + str(v.ping()))
963 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
966 reset_idler(v, vstatus)
968 # This main loop was hideous and the work of the devil.
969 # This has now been fixed (mostly) - mtearle
972 # notes for later surgery
973 # (event, counter, ' ')
977 # ( return state - not currently implemented )
983 except DispenseDatabaseException, e:
984 logging.error('Database error: '+str(e))
986 timeout = time_to_next_update(vstatus)
987 e = v.next_event(timeout)
990 run_handler(event, params, v, vstatus)
992 # logging.debug('Got event: ' + repr(e))
995 def run_handler(event, params, v, vstatus):
996 handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
998 handler(vstatus.state, event, params, v, vstatus)
1000 def connect_to_vend(options, cf):
1003 logging.info('Connecting to vending machine using LAT')
1004 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
1005 rfh, wfh = latclient.get_fh()
1006 elif options.use_serial:
1007 # Open vending machine via serial.
1008 logging.info('Connecting to vending machine using serial')
1009 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
1010 rfh,wfh = serialclient.get_fh()
1012 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
1013 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
1015 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
1016 sock.connect((options.host, options.port))
1017 rfh = sock.makefile('r')
1018 wfh = sock.makefile('w')
1025 from optparse import OptionParser
1027 op = OptionParser(usage="%prog [OPTION]...")
1028 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')
1029 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
1030 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
1031 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
1032 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
1033 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
1034 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
1035 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
1036 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
1037 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
1038 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
1039 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
1040 options, args = op.parse_args()
1043 op.error('extra command line arguments: ' + ' '.join(args))
1048 'DBServer': ('Database', 'Server'),
1049 'DBName': ('Database', 'Name'),
1050 'DBUser': ('VendingMachine', 'DBUser'),
1051 'DBPassword': ('VendingMachine', 'DBPassword'),
1053 'ServiceName': ('VendingMachine', 'ServiceName'),
1054 'ServicePassword': ('VendingMachine', 'Password'),
1056 'ServerName': ('DecServer', 'Name'),
1057 'ConnectPassword': ('DecServer', 'ConnectPassword'),
1058 'PrivPassword': ('DecServer', 'PrivPassword'),
1061 class VendConfigFile:
1062 def __init__(self, config_file, options):
1064 cp = ConfigParser.ConfigParser()
1065 cp.read(config_file)
1067 for option in options:
1068 section, name = options[option]
1069 value = cp.get(section, name)
1070 self.__dict__[option] = value
1072 except ConfigParser.Error, e:
1073 raise SystemExit("Error reading config file "+config_file+": " + str(e))
1075 def create_pid_file(name):
1077 pid_file = file(name, 'w')
1078 pid_file.write('%d\n'%os.getpid())
1081 logging.warning('unable to write to pid file '+name+': '+str(e))
1084 def do_nothing(signum, stack):
1085 signal.signal(signum, do_nothing)
1086 def stop_server(signum, stack): raise KeyboardInterrupt
1087 signal.signal(signal.SIGHUP, do_nothing)
1088 signal.signal(signal.SIGTERM, stop_server)
1089 signal.signal(signal.SIGINT, stop_server)
1091 options = parse_args()
1092 config_opts = VendConfigFile(options.config_file, config_options)
1093 if options.daemon: become_daemon()
1094 set_up_logging(options)
1095 if options.pid_file != '': create_pid_file(options.pid_file)
1097 return options, config_opts
1099 def clean_up_nicely(options, config_opts):
1100 if options.pid_file != '':
1102 os.unlink(options.pid_file)
1103 logging.debug('Removed pid file '+options.pid_file)
1104 except OSError: pass # if we can't delete it, meh
1106 def set_up_logging(options):
1107 logger = logging.getLogger()
1109 if not options.daemon:
1110 stderr_logger = logging.StreamHandler(sys.stderr)
1111 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1112 logger.addHandler(stderr_logger)
1114 if options.log_file != '':
1116 file_logger = logging.FileHandler(options.log_file)
1117 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1118 logger.addHandler(file_logger)
1120 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1122 if options.syslog != None:
1123 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1124 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1125 logger.addHandler(sys_logger)
1128 logger.setLevel(logging.WARNING)
1129 elif options.verbose:
1130 logger.setLevel(logging.DEBUG)
1132 logger.setLevel(logging.INFO)
1134 def become_daemon():
1135 dev_null = file('/dev/null')
1136 fd = dev_null.fileno()
1145 raise SystemExit('failed to fork: '+str(e))
1147 def do_vend_server(options, config_opts):
1150 rfh, wfh = connect_to_vend(options, config_opts)
1151 except (SerialClientException, socket.error), e:
1152 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1154 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1155 logging.info("Trying again in 5 seconds.")
1159 # run_forever(rfh, wfh, options, config_opts)
1162 run_forever(rfh, wfh, options, config_opts)
1163 except VendingException:
1164 logging.error("Connection died, trying again...")
1165 logging.info("Trying again in 5 seconds.")
1168 if __name__ == '__main__':
1169 options, config_opts = set_stuff_up()
1172 logging.warning('Starting Vend Server')
1173 do_vend_server(options, config_opts)
1174 logging.error('Vend Server finished unexpectedly, restarting')
1175 except KeyboardInterrupt:
1176 logging.info("Killed by signal, cleaning up")
1177 clean_up_nicely(options, config_opts)
1178 logging.warning("Vend Server stopped")
1183 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1184 tb = format_tb(exc_traceback, 20)
1187 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1188 logging.critical("Message: " + str(exc_value))
1189 logging.critical("Traceback:")
1191 for line in event.split('\n'):
1192 logging.critical(' '+line)
1193 logging.critical("This message should be considered a bug in the Vend Server.")
1194 logging.critical("Please report this to someone who can fix it.")
1196 logging.warning("Trying again anyway (might not help, but hey...)")