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.
34 The MIFARE card reader bought to you by:
37 Bug Hunting and hardware maintenance by:
40 For a good time call +61 8 6488 3901
62 STATE_GRANDFATHER_CLOCK,
68 class DispenseDatabaseException(Exception): pass
70 class DispenseDatabase:
71 def __init__(self, vending_machine, host, name, user, password):
72 self.vending_machine = vending_machine
73 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
74 self.db.query('LISTEN vend_requests')
76 def process_requests(self):
77 logging.debug('database processing')
78 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
80 outstanding = self.db.query(query).getresult()
81 except (pg.error,), db_err:
82 raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
83 for (id, slot) in outstanding:
84 (worked, code, string) = self.vending_machine.vend(slot)
85 logging.debug (str((worked, code, string)))
87 query = 'SELECT vend_success(%s)'%id
88 self.db.query(query).getresult()
90 query = 'SELECT vend_failed(%s)'%id
91 self.db.query(query).getresult()
93 def handle_events(self):
94 notifier = self.db.getnotify()
95 while notifier is not None:
96 self.process_requests()
97 notify = self.db.getnotify()
99 def scroll_options(username, mk, welcome = False):
102 acct, unused = Popen(['dispense', 'acct', username], close_fds=True, stdout=PIPE).communicate()
103 # this is fucking appalling
104 balance = acct[acct.find("$")+1:acct.find("(")].strip()
106 msg = [(center('WELCOME'), False, TEXT_SPEED),
107 (center(username), False, TEXT_SPEED),
108 (center(balance), False, TEXT_SPEED),]
111 choices = ' '*10+'CHOICES: '
115 for i in range(0, 7):
116 args = ('dispense', 'iteminfo', 'coke:%i' % i)
117 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
118 m = re.match("\s*[a-z]+:\d+\s+(\d+)\.(\d\d)\s+([^\n]+)", info)
119 cents = int(m.group(1))*100 + int(m.group(2))
120 cokes.append('%i %i %s' % (i, cents, m.group(3)));
124 (slot_num, price, slot_name) = c.split(' ', 2)
125 if slot_name == 'dead': continue
126 choices += '%s-(%sc)-%s8 '%(slot_name, price, slot_num)
128 # we don't want to print snacks for now since it'll be too large
129 # and there's physical bits of paper in the machine anyway - matt
131 # snacks = get_snacks()
135 # for slot, ( name, price ) in snacks.items():
136 # choices += '%s8-%s (%sc) ' % ( slot, name, price )
138 choices += '55-DOOR '
139 choices += 'OR ANOTHER SNACK. '
140 choices += '99 TO READ AGAIN. '
141 choices += 'CHOICE? '
142 msg.append((choices, False, None))
149 def _check_pin(uid, pin):
153 print "_check_pin('",uid,"',---)"
156 info = pwd.getpwuid(uid)
158 logging.info('getting pin for uid %d: user not in password file'%uid)
160 if info.pw_dir == None: return False
161 pinfile = os.path.join(info.pw_dir, '.pin')
165 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
168 logging.info('getting pin for uid %d: .pin has wrong permissions. Fixing.'%uid)
169 os.chmod(pinfile, 0600)
173 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
175 pinstr = f.readline()
177 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
178 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
182 _pin_uname = info.pw_name
185 if pin == int(pinstr):
186 logging.info("Pin correct for %d",uid)
188 logging.info("Pin incorrect for %d",uid)
189 return pin == int(pinstr)
191 def acct_is_disabled(name=None):
195 acct, unused = Popen(['dispense', 'acct', _pin_uname], close_fds=True, stdout=PIPE).communicate()
196 # this is fucking appalling
197 flags = acct[acct.find("(")+1:acct.find(")")].strip()
198 if 'disabled' in flags:
200 if 'internal' in flags:
204 def has_good_pin(uid):
205 return _check_pin(uid, None) != None
207 def verify_user_pin(uid, pin, skip_pin_check=False):
208 if skip_pin_check or _check_pin(uid, pin) == True:
209 info = pwd.getpwuid(uid)
211 if acct_is_disabled(info.pw_name):
212 logging.info('refused mifare for disabled acct uid %d (%s)'%(uid,info.pw_name))
214 logging.info('accepted mifare for uid %d (%s)'%(uid,info.pw_name))
216 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
219 logging.info('refused pin for uid %d'%(uid))
225 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
226 choice = int(random()*len(messages))
227 msg = messages[choice]
228 left = range(len(msg))
229 for i in range(len(msg)):
230 if msg[i] == ' ': left.remove(i)
234 for i in range(0, len(msg)):
240 s += chr(int(random()*26)+ord('A'))
249 return ' '*((LEN-len(str))/2)+str
261 GrayIdler(v,one="*",zero="-"),
262 GrayIdler(v,one="/",zero="\\"),
263 GrayIdler(v,one="X",zero="O"),
264 GrayIdler(v,one="*",zero="-",reorder=1),
265 GrayIdler(v,one="/",zero="\\",reorder=1),
266 GrayIdler(v,one="X",zero="O",reorder=1),
272 StringIdler(v), # Hello Cruel World
273 StringIdler(v, text="Kill 'em all", repeat=False),
274 StringIdler(v, text=CREDITS),
275 StringIdler(v, text=str(math.pi) + " "),
276 StringIdler(v, text=str(math.e) + " "),
277 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),
278 # "Hello World" in brainfuck
279 StringIdler(v, text=">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++."),
283 FileIdler(v, '/usr/share/common-licenses/GPL-2',affinity=2),
285 PipeIdler(v, "/usr/bin/getent", "passwd"),
286 FortuneIdler(v,affinity=20),
291 def reset_idler(v, vstatus, t = None):
293 idler = GreetingIdler(v, t)
294 vstatus.time_of_next_idlestep = time()+idler.next()
295 vstatus.time_of_next_idler = None
296 vstatus.time_to_autologout = None
297 vstatus.change_state(STATE_IDLE, 1)
302 # Implementation of the King Of the Hill algorithm from;
303 # http://eli.thegreenplace.net/2010/01/22/weighted-random-generation-in-python/
305 #def weighted_choice_king(weights):
308 # for i, w in enumerate(weights):
310 # if random.random() * total < w:
318 for choice in idlers:
319 weight = choice.affinity()
321 if random() * total < weight:
329 def idle_step(vstatus):
333 vstatus.time_of_next_idler = time() + 30
334 nextidle = idler.next()
336 nextidle = IDLE_SPEED
337 vstatus.time_of_next_idlestep = time()+nextidle
340 def __init__(self,v):
341 self.state_table = {}
342 self.state = STATE_IDLE
345 self.mk = MessageKeeper(v)
349 self.cur_selection = ''
350 self.time_to_autologout = None
352 self.last_timeout_refresh = None
354 def change_state(self,newstate,newcounter=None):
355 if self.state != newstate:
356 #print "Changing state from: ",
360 self.state = newstate
362 if newcounter is not None and self.counter != newcounter:
363 #print "Changing counter from: ",
367 self.counter = newcounter
371 def handle_tick_event(event, params, v, vstatus):
372 # don't care right now.
375 def handle_switch_event(event, params, v, vstatus):
376 # don't care right now.
380 def do_nothing(state, event, params, v, vstatus):
381 print "doing nothing (s,e,p)", state, " ", event, " ", params
384 def handle_getting_uid_idle(state, event, params, v, vstatus):
385 # don't care right now.
388 def handle_getting_pin_idle(state, event, params, v, vstatus):
389 # don't care right now.
392 def handle_get_selection_idle(state, event, params, v, vstatus):
394 # don't care right now.
396 ### State logging out ..
397 if vstatus.time_to_autologout != None:
398 time_left = vstatus.time_to_autologout - time()
399 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
400 vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
401 vstatus.last_timeout_refresh = int(time_left)
402 vstatus.cur_selection = ''
404 if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
405 vstatus.time_to_autologout = None
406 vstatus.cur_user = ''
408 vstatus.cur_selection = ''
410 reset_idler(v, vstatus)
412 ### State fully logged out ... reset variables
413 if vstatus.time_to_autologout and not vstatus.mk.done():
414 vstatus.time_to_autologout = None
415 if vstatus.cur_user == '' and vstatus.time_to_autologout:
416 vstatus.time_to_autologout = None
419 if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
421 vstatus.time_to_autologout = time() + 15
422 vstatus.last_timeout_refresh = None
424 ## FIXME - this may need to be elsewhere.....
426 vstatus.mk.update_display()
430 def handle_get_selection_key(state, event, params, v, vstatus):
433 if len(vstatus.cur_selection) == 0:
436 vstatus.cur_user = ''
437 vstatus.cur_selection = ''
439 vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
440 reset_idler(v, vstatus, 2)
442 vstatus.cur_selection += chr(key + ord('0'))
443 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
444 vstatus.time_to_autologout = None
445 elif len(vstatus.cur_selection) == 1:
447 vstatus.cur_selection = ''
448 vstatus.time_to_autologout = None
449 scroll_options(vstatus.username, vstatus.mk)
452 vstatus.cur_selection += chr(key + ord('0'))
454 make_selection(v,vstatus)
455 vstatus.cur_selection = ''
456 vstatus.time_to_autologout = time() + 8
457 vstatus.last_timeout_refresh = None
460 price_check(v,vstatus)
461 vstatus.cur_selection = ''
462 vstatus.time_to_autologout = None
463 vstatus.last_timeout_refresh = None
465 def make_selection(v, vstatus):
466 # should use sudo here
467 if vstatus.cur_selection == '55':
468 vstatus.mk.set_message('OPENSESAME')
469 logging.info('dispensing a door for %s'%vstatus.username)
471 #ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
472 ret = os.system('dispense -u "%s" door'%vstatus.username)
474 ret = os.system('dispense door')
476 logging.info('door opened')
477 vstatus.mk.set_message(center('DOOR OPEN'))
479 logging.warning('user %s tried to dispense a bad door'%vstatus.username)
480 vstatus.mk.set_message(center('BAD DOOR'))
482 elif vstatus.cur_selection == '81':
484 elif vstatus.cur_selection == '99':
485 scroll_options(vstatus.username, vstatus.mk)
486 vstatus.cur_selection = ''
488 elif vstatus.cur_selection[1] == '8':
489 v.display('GOT DRINK?')
490 if ((os.system('dispense -u "%s" coke:%s'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
491 v.display('SEEMS NOT')
493 v.display('GOT DRINK!')
495 # first see if it's a named slot
497 price, shortname, name = get_snack( vstatus.cur_selection )
499 price, shortname, name = get_snack( '--' )
500 dollarprice = "$%.2f" % ( price / 100.0 )
501 v.display(vstatus.cur_selection+' - %s'%dollarprice)
502 # exitcode = os.system('dispense -u "%s" give \>snacksales %d "%s"'%(vstatus.username, price, name)) >> 8
503 # exitcode = os.system('dispense -u "%s" give \>sales\:snack %d "%s"'%(vstatus.username, price, name)) >> 8
504 exitcode = os.system('dispense -u "%s" snack:%s'%(vstatus.username, vstatus.cur_selection)) >> 8
506 # magic dispense syslog service
507 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
508 (worked, code, string) = v.vend(vstatus.cur_selection)
510 v.display('THANK YOU')
512 print "Vend Failed:", code, string
513 v.display('VEND FAIL')
514 elif (exitcode == 5): # RV_BALANCE
515 v.display('NO MONEY?')
516 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
517 v.display('EMPTY SLOT')
518 elif (exitcode == 1): # RV_BADITEM (Dead slot)
519 v.display('EMPTY SLOT')
521 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
522 v.display('UNK ERROR')
526 def price_check(v, vstatus):
527 if vstatus.cur_selection[1] == '8':
528 args = ('dispense', 'iteminfo', 'coke:' + vstatus.cur_selection[0])
529 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
530 dollarprice = re.match("\s*[a-z]+:\d+\s+(\d+\.\d\d)\s+([^\n]+)", info).group(1)
532 # first see if it's a named slot
534 price, shortname, name = get_snack( vstatus.cur_selection )
536 price, shortname, name = get_snack( '--' )
537 dollarprice = "$%.2f" % ( price / 100.0 )
538 v.display(vstatus.cur_selection+' - %s'%dollarprice)
541 def handle_getting_pin_key(state, event, params, v, vstatus):
542 #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
544 if len(vstatus.cur_pin) < PIN_LENGTH:
546 if vstatus.cur_pin == '':
547 vstatus.cur_user = ''
548 reset_idler(v, vstatus)
552 vstatus.mk.set_message('PIN: ')
554 vstatus.cur_pin += chr(key + ord('0'))
555 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
556 if len(vstatus.cur_pin) == PIN_LENGTH:
557 vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
560 vstatus.cur_selection = ''
561 vstatus.change_state(STATE_GET_SELECTION)
562 scroll_options(vstatus.username, vstatus.mk, True)
566 vstatus.mk.set_messages(
567 [(center('BAD PIN'), False, 1.0),
568 (center('SORRY'), False, 0.5)])
569 vstatus.cur_user = ''
572 reset_idler(v, vstatus, 2)
577 def handle_getting_uid_key(state, event, params, v, vstatus):
578 #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
581 # complicated key handling here:
583 if len(vstatus.cur_user) == 0 and key == 9:
584 vstatus.cur_selection = ''
585 vstatus.time_to_autologout = None
586 vstatus.mk.set_message('PRICECHECK')
588 scroll_options('', vstatus.mk)
589 vstatus.change_state(STATE_GET_SELECTION)
592 if len(vstatus.cur_user) <8:
594 vstatus.cur_user = ''
596 reset_idler(v, vstatus)
598 vstatus.cur_user += chr(key + ord('0'))
599 #logging.info('dob: '+vstatus.cur_user)
600 if len(vstatus.cur_user) > 5:
601 vstatus.mk.set_message('>'+vstatus.cur_user)
603 vstatus.mk.set_message('UID: '+vstatus.cur_user)
605 if len(vstatus.cur_user) == 5:
606 uid = int(vstatus.cur_user)
609 logging.info('user '+vstatus.cur_user+' has a bad PIN')
615 Welcome to Picklevision Sytems, Sunnyvale, CA
617 Greetings Professor Falken.
622 Shall we play a game?
625 Please choose from the following menu:
632 6. Toxic and Biochemical Warfare
633 7. Global Thermonuclear War
637 Wouldn't you prefer a nice game of chess?
639 """.replace('\n',' ')
640 vstatus.mk.set_messages([(pfalken, False, 10)])
641 vstatus.cur_user = ''
644 reset_idler(v, vstatus, 10)
648 if not has_good_pin(uid):
649 logging.info('user '+vstatus.cur_user+' has a bad PIN')
650 vstatus.mk.set_messages(
651 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
652 vstatus.cur_user = ''
655 reset_idler(v, vstatus, 3)
659 if acct_is_disabled():
660 logging.info('user '+vstatus.cur_user+' is disabled')
661 vstatus.mk.set_messages(
662 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
663 vstatus.cur_user = ''
666 reset_idler(v, vstatus, 3)
671 vstatus.mk.set_message('PIN: ')
672 logging.info('need pin for user %s'%vstatus.cur_user)
673 vstatus.change_state(STATE_GETTING_PIN)
677 def handle_idle_key(state, event, params, v, vstatus):
678 #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
683 vstatus.cur_user = ''
684 reset_idler(v, vstatus)
687 vstatus.change_state(STATE_GETTING_UID)
688 run_handler(event, key, v, vstatus)
691 def handle_idle_tick(state, event, params, v, vstatus):
693 if vstatus.mk.done():
696 if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
697 vstatus.time_of_next_idler = time() + 30
702 vstatus.mk.update_display()
704 vstatus.change_state(STATE_GRANDFATHER_CLOCK)
705 run_handler(event, params, v, vstatus)
708 def beep_on(when, before=0):
709 start = int(when - before)
713 if now >= start and now <= end:
717 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
718 ### check for interesting times
721 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
722 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
723 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
724 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
726 hourfromnow = localtime(time() + 3600)
728 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
729 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
730 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
732 ## check for X seconds to the hour
733 ## if case, update counter to 2
734 if beep_on(onthehour,15) \
735 or beep_on(halfhour,0) \
736 or beep_on(quarterhour,0) \
737 or beep_on(threequarterhour,0) \
738 or beep_on(fivetothehour,0):
739 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
740 run_handler(event, params, v, vstatus)
742 vstatus.change_state(STATE_IDLE)
744 def handle_grandfather_tick(state, event, params, v, vstatus):
748 ### we live in interesting times
751 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
752 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
753 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
754 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
756 hourfromnow = localtime(time() + 3600)
758 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
759 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
760 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
763 #print "when it fashionable to wear a onion on your hip"
765 if beep_on(onthehour,15):
767 next_hour=((hourfromnow[3] + 11) % 12) + 1
768 if onthehour - time() < next_hour and onthehour - time() > 0:
773 msg.append(("DING!", False, None))
775 msg.append((" DING!", False, None))
776 elif int(onthehour - time()) == 0:
778 msg.append((" BONG!", False, None))
779 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
780 elif beep_on(halfhour,0):
783 msg.append((" HALFHOUR ", False, 50))
784 elif beep_on(quarterhour,0):
787 msg.append((" QTR HOUR ", False, 50))
788 elif beep_on(threequarterhour,0):
791 msg.append((" 3 QTR HR ", False, 50))
792 elif beep_on(fivetothehour,0):
795 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
799 ## check for X seconds to the hour
802 vstatus.mk.set_messages(msg)
805 vstatus.mk.update_display()
806 ## if no longer case, return to idle
808 ## change idler to be clock
809 if go_idle and vstatus.mk.done():
810 vstatus.change_state(STATE_IDLE,1)
812 def handle_door_idle(state, event, params, v, vstatus):
813 def twiddle(clock,v,wise = 2):
815 v.display("-FEED ME-")
816 elif (clock % 4 == 1+wise):
817 v.display("\\FEED ME/")
818 elif (clock % 4 == 2):
819 v.display("-FEED ME-")
820 elif (clock % 4 == 3-wise):
821 v.display("/FEED ME\\")
823 # don't care right now.
826 if ((now % 60 % 2) == 0):
829 twiddle(now, v, wise=0)
832 def handle_door_event(state, event, params, v, vstatus):
833 if params == 0: #door open
834 vstatus.change_state(STATE_DOOR_OPENING)
835 logging.warning("Entering open door mode")
836 v.display("-FEED ME-")
838 vstatus.cur_user = ''
840 elif params == 1: #door closed
841 vstatus.change_state(STATE_DOOR_CLOSING)
842 reset_idler(v, vstatus, 3)
844 logging.warning('Leaving open door mode')
845 v.display("-YUM YUM!-")
849 def handle_mifare_event(state, event, params, v, vstatus):
852 # Translate card_id into uid.
853 if card_id == None or card_id == _last_card_id:
856 _last_card_id = card_id
859 vstatus.cur_user = get_uid(card_id)
860 logging.info('Mapped card id to uid %s'%vstatus.cur_user)
861 vstatus.username = get_uname(vstatus.cur_user)
862 if acct_is_disabled(vstatus.username):
863 vstatus.username = '-disabled-'
865 vstatus.username = None
866 if vstatus.username == '-disabled-':
868 vstatus.mk.set_messages(
869 [(center('ACCT DISABLED'), False, 1.0),
870 (center('SORRY'), False, 0.5)])
871 vstatus.cur_user = ''
873 vstatus.username = None
875 reset_idler(v, vstatus, 2)
877 elif vstatus.username:
879 vstatus.cur_selection = ''
880 vstatus.change_state(STATE_GET_SELECTION)
881 scroll_options(vstatus.username, vstatus.mk, True)
885 vstatus.mk.set_messages(
886 [(center('BAD CARD'), False, 1.0),
887 (center('SORRY'), False, 0.5)])
888 vstatus.cur_user = ''
892 reset_idler(v, vstatus, 2)
895 def handle_mifare_add_user_event(state, event, params, v, vstatus):
899 # Translate card_id into uid.
900 if card_id == None or card_id == _last_card_id:
903 _last_card_id = card_id
906 if get_uid(card_id) != None:
907 vstatus.mk.set_messages(
908 [(center('ALREADY'), False, 0.5),
909 (center('ENROLLED'), False, 0.5)])
911 # scroll_options(vstatus.username, vstatus.mk)
916 logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
917 set_card_id(vstatus.cur_user, card_id)
918 vstatus.mk.set_messages(
919 [(center('CARD'), False, 0.5),
920 (center('ENROLLED'), False, 0.5)])
922 # scroll_options(vstatus.username, vstatus.mk)
924 def return_to_idle(state,event,params,v,vstatus):
925 reset_idler(v, vstatus)
927 def create_state_table(vstatus):
928 vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
929 vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
930 vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
931 vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
933 vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
934 vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
935 vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
936 vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
938 vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
939 vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
940 vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
941 vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
943 vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
944 vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = handle_door_event
945 vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
946 vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
948 vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
949 vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = handle_door_event
950 vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
951 vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
953 vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
954 vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = handle_door_event
955 vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
956 vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
958 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
959 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
960 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = handle_door_event
961 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = handle_door_event
962 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
963 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
964 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
966 def get_state_table_handler(vstatus, state, event, counter):
967 return vstatus.state_table[(state,event,counter)]
969 def time_to_next_update(vstatus):
970 idle_update = vstatus.time_of_next_idlestep - time()
971 if not vstatus.mk.done() and vstatus.mk.next_update is not None:
972 mk_update = vstatus.mk.next_update - time()
973 if mk_update < idle_update:
974 idle_update = mk_update
977 def run_forever(rfh, wfh, options, cf):
978 v = VendingMachine(rfh, wfh, USE_MIFARE)
979 vstatus = VendState(v)
980 create_state_table(vstatus)
982 logging.debug('PING is ' + str(v.ping()))
984 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
987 reset_idler(v, vstatus)
989 # This main loop was hideous and the work of the devil.
990 # This has now been fixed (mostly) - mtearle
993 # notes for later surgery
994 # (event, counter, ' ')
998 # ( return state - not currently implemented )
1004 except DispenseDatabaseException, e:
1005 logging.error('Database error: '+str(e))
1007 timeout = time_to_next_update(vstatus)
1008 e = v.next_event(timeout)
1011 run_handler(event, params, v, vstatus)
1013 # logging.debug('Got event: ' + repr(e))
1016 def run_handler(event, params, v, vstatus):
1017 handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
1019 handler(vstatus.state, event, params, v, vstatus)
1021 def connect_to_vend(options, cf):
1024 logging.info('Connecting to vending machine using LAT')
1025 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
1026 rfh, wfh = latclient.get_fh()
1027 elif options.use_serial:
1028 # Open vending machine via serial.
1029 logging.info('Connecting to vending machine using serial')
1030 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
1031 rfh,wfh = serialclient.get_fh()
1033 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
1034 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
1036 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
1037 sock.connect((options.host, options.port))
1038 rfh = sock.makefile('r')
1039 wfh = sock.makefile('w')
1046 from optparse import OptionParser
1048 op = OptionParser(usage="%prog [OPTION]...")
1049 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')
1050 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
1051 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
1052 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
1053 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
1054 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
1055 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
1056 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
1057 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
1058 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
1059 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
1060 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
1061 options, args = op.parse_args()
1064 op.error('extra command line arguments: ' + ' '.join(args))
1069 'DBServer': ('Database', 'Server'),
1070 'DBName': ('Database', 'Name'),
1071 'DBUser': ('VendingMachine', 'DBUser'),
1072 'DBPassword': ('VendingMachine', 'DBPassword'),
1074 'ServiceName': ('VendingMachine', 'ServiceName'),
1075 'ServicePassword': ('VendingMachine', 'Password'),
1077 'ServerName': ('DecServer', 'Name'),
1078 'ConnectPassword': ('DecServer', 'ConnectPassword'),
1079 'PrivPassword': ('DecServer', 'PrivPassword'),
1082 class VendConfigFile:
1083 def __init__(self, config_file, options):
1085 cp = ConfigParser.ConfigParser()
1086 cp.read(config_file)
1088 for option in options:
1089 section, name = options[option]
1090 value = cp.get(section, name)
1091 self.__dict__[option] = value
1093 except ConfigParser.Error, e:
1094 raise SystemExit("Error reading config file "+config_file+": " + str(e))
1096 def create_pid_file(name):
1098 pid_file = file(name, 'w')
1099 pid_file.write('%d\n'%os.getpid())
1102 logging.warning('unable to write to pid file '+name+': '+str(e))
1105 def do_nothing(signum, stack):
1106 signal.signal(signum, do_nothing)
1107 def stop_server(signum, stack): raise KeyboardInterrupt
1108 signal.signal(signal.SIGHUP, do_nothing)
1109 signal.signal(signal.SIGTERM, stop_server)
1110 signal.signal(signal.SIGINT, stop_server)
1112 options = parse_args()
1113 config_opts = VendConfigFile(options.config_file, config_options)
1114 if options.daemon: become_daemon()
1115 set_up_logging(options)
1116 if options.pid_file != '': create_pid_file(options.pid_file)
1118 return options, config_opts
1120 def clean_up_nicely(options, config_opts):
1121 if options.pid_file != '':
1123 os.unlink(options.pid_file)
1124 logging.debug('Removed pid file '+options.pid_file)
1125 except OSError: pass # if we can't delete it, meh
1127 def set_up_logging(options):
1128 logger = logging.getLogger()
1130 if not options.daemon:
1131 stderr_logger = logging.StreamHandler(sys.stderr)
1132 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1133 logger.addHandler(stderr_logger)
1135 if options.log_file != '':
1137 file_logger = logging.FileHandler(options.log_file)
1138 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1139 logger.addHandler(file_logger)
1141 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1143 if options.syslog != None:
1144 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1145 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1146 logger.addHandler(sys_logger)
1149 logger.setLevel(logging.WARNING)
1150 elif options.verbose:
1151 logger.setLevel(logging.DEBUG)
1153 logger.setLevel(logging.INFO)
1155 def become_daemon():
1156 dev_null = file('/dev/null')
1157 fd = dev_null.fileno()
1166 raise SystemExit('failed to fork: '+str(e))
1168 def do_vend_server(options, config_opts):
1171 rfh, wfh = connect_to_vend(options, config_opts)
1172 except (SerialClientException, socket.error), e:
1173 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1175 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1176 logging.info("Trying again in 5 seconds.")
1180 # run_forever(rfh, wfh, options, config_opts)
1183 run_forever(rfh, wfh, options, config_opts)
1184 except VendingException:
1185 logging.error("Connection died, trying again...")
1186 logging.info("Trying again in 5 seconds.")
1190 def main(argv=None):
1191 options, config_opts = set_stuff_up()
1194 logging.warning('Starting Vend Server')
1195 do_vend_server(options, config_opts)
1196 logging.error('Vend Server finished unexpectedly, restarting')
1197 except KeyboardInterrupt:
1198 logging.info("Killed by signal, cleaning up")
1199 clean_up_nicely(options, config_opts)
1200 logging.warning("Vend Server stopped")
1205 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1206 tb = format_tb(exc_traceback, 20)
1209 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1210 logging.critical("Message: " + str(exc_value))
1211 logging.critical("Traceback:")
1213 for line in event.split('\n'):
1214 logging.critical(' '+line)
1215 logging.critical("This message should be considered a bug in the Vend Server.")
1216 logging.critical("Please report this to someone who can fix it.")
1218 logging.warning("Trying again anyway (might not help, but hey...)")
1220 if __name__ == '__main__':