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):
393 # don't care right now.
395 ### State logging out ..
396 if vstatus.time_to_autologout != None:
397 time_left = vstatus.time_to_autologout - time()
398 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
399 vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
400 vstatus.last_timeout_refresh = int(time_left)
401 vstatus.cur_selection = ''
403 if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
404 vstatus.time_to_autologout = None
405 vstatus.cur_user = ''
407 vstatus.cur_selection = ''
409 reset_idler(v, vstatus)
411 ### State fully logged out ... reset variables
412 if vstatus.time_to_autologout and not vstatus.mk.done():
413 vstatus.time_to_autologout = None
414 if vstatus.cur_user == '' and vstatus.time_to_autologout:
415 vstatus.time_to_autologout = None
418 if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
420 vstatus.time_to_autologout = time() + 15
421 vstatus.last_timeout_refresh = None
423 ## FIXME - this may need to be elsewhere.....
425 vstatus.mk.update_display()
429 def handle_get_selection_key(state, event, params, v, vstatus):
431 if len(vstatus.cur_selection) == 0:
434 vstatus.cur_user = ''
435 vstatus.cur_selection = ''
437 vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
438 reset_idler(v, vstatus, 2)
440 vstatus.cur_selection += chr(key + ord('0'))
441 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
442 vstatus.time_to_autologout = None
443 elif len(vstatus.cur_selection) == 1:
445 vstatus.cur_selection = ''
446 vstatus.time_to_autologout = None
447 scroll_options(vstatus.username, vstatus.mk)
450 vstatus.cur_selection += chr(key + ord('0'))
452 make_selection(v,vstatus)
453 vstatus.cur_selection = ''
454 vstatus.time_to_autologout = time() + 8
455 vstatus.last_timeout_refresh = None
458 price_check(v,vstatus)
459 vstatus.cur_selection = ''
460 vstatus.time_to_autologout = None
461 vstatus.last_timeout_refresh = None
463 def make_selection(v, vstatus):
464 # should use sudo here
465 if vstatus.cur_selection == '55':
466 vstatus.mk.set_message('OPENSESAME')
467 logging.info('dispensing a door for %s'%vstatus.username)
469 #ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
470 ret = os.system('dispense -u "%s" door'%vstatus.username)
472 ret = os.system('dispense door')
474 logging.info('door opened')
475 vstatus.mk.set_message(center('DOOR OPEN'))
477 logging.warning('user %s tried to dispense a bad door'%vstatus.username)
478 vstatus.mk.set_message(center('BAD DOOR'))
480 elif vstatus.cur_selection == '81':
482 elif vstatus.cur_selection == '99':
483 scroll_options(vstatus.username, vstatus.mk)
484 vstatus.cur_selection = ''
486 elif vstatus.cur_selection[1] == '8':
487 v.display('GOT DRINK?')
488 if ((os.system('dispense -u "%s" coke:%s'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
489 v.display('SEEMS NOT')
491 v.display('GOT DRINK!')
493 # first see if it's a named slot
495 price, shortname, name = get_snack( vstatus.cur_selection )
497 price, shortname, name = get_snack( '--' )
498 dollarprice = "$%.2f" % ( price / 100.0 )
499 v.display(vstatus.cur_selection+' - %s'%dollarprice)
500 # exitcode = os.system('dispense -u "%s" give \>snacksales %d "%s"'%(vstatus.username, price, name)) >> 8
501 # exitcode = os.system('dispense -u "%s" give \>sales\:snack %d "%s"'%(vstatus.username, price, name)) >> 8
502 exitcode = os.system('dispense -u "%s" snack:%s'%(vstatus.username, vstatus.cur_selection)) >> 8
504 # magic dispense syslog service
505 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
506 (worked, code, string) = v.vend(vstatus.cur_selection)
508 v.display('THANK YOU')
510 print "Vend Failed:", code, string
511 v.display('VEND FAIL')
512 elif (exitcode == 5): # RV_BALANCE
513 v.display('NO MONEY?')
514 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
515 v.display('EMPTY SLOT')
516 elif (exitcode == 1): # RV_BADITEM (Dead slot)
517 v.display('EMPTY SLOT')
519 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
520 v.display('UNK ERROR')
524 def price_check(v, vstatus):
525 if vstatus.cur_selection[1] == '8':
526 args = ('dispense', 'iteminfo', 'coke:' + vstatus.cur_selection[0])
527 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
528 dollarprice = re.match("\s*[a-z]+:\d+\s+(\d+\.\d\d)\s+([^\n]+)", info).group(1)
530 # first see if it's a named slot
532 price, shortname, name = get_snack( vstatus.cur_selection )
534 price, shortname, name = get_snack( '--' )
535 dollarprice = "$%.2f" % ( price / 100.0 )
536 v.display(vstatus.cur_selection+' - %s'%dollarprice)
539 def handle_getting_pin_key(state, event, params, v, vstatus):
540 #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
542 if len(vstatus.cur_pin) < PIN_LENGTH:
544 if vstatus.cur_pin == '':
545 vstatus.cur_user = ''
546 reset_idler(v, vstatus)
550 vstatus.mk.set_message('PIN: ')
552 vstatus.cur_pin += chr(key + ord('0'))
553 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
554 if len(vstatus.cur_pin) == PIN_LENGTH:
555 vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
558 vstatus.cur_selection = ''
559 vstatus.change_state(STATE_GET_SELECTION)
560 scroll_options(vstatus.username, vstatus.mk, True)
564 vstatus.mk.set_messages(
565 [(center('BAD PIN'), False, 1.0),
566 (center('SORRY'), False, 0.5)])
567 vstatus.cur_user = ''
570 reset_idler(v, vstatus, 2)
575 def handle_getting_uid_key(state, event, params, v, vstatus):
576 #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
579 # complicated key handling here:
581 if len(vstatus.cur_user) == 0 and key == 9:
582 vstatus.cur_selection = ''
583 vstatus.time_to_autologout = None
584 vstatus.mk.set_message('PRICECHECK')
586 scroll_options('', vstatus.mk)
587 vstatus.change_state(STATE_GET_SELECTION)
590 if len(vstatus.cur_user) <8:
592 vstatus.cur_user = ''
594 reset_idler(v, vstatus)
596 vstatus.cur_user += chr(key + ord('0'))
597 #logging.info('dob: '+vstatus.cur_user)
598 if len(vstatus.cur_user) > 5:
599 vstatus.mk.set_message('>'+vstatus.cur_user)
601 vstatus.mk.set_message('UID: '+vstatus.cur_user)
603 if len(vstatus.cur_user) == 5:
604 uid = int(vstatus.cur_user)
607 logging.info('user '+vstatus.cur_user+' has a bad PIN')
613 Welcome to Picklevision Sytems, Sunnyvale, CA
615 Greetings Professor Falken.
620 Shall we play a game?
623 Please choose from the following menu:
630 6. Toxic and Biochemical Warfare
631 7. Global Thermonuclear War
635 Wouldn't you prefer a nice game of chess?
637 """.replace('\n',' ')
638 vstatus.mk.set_messages([(pfalken, False, 10)])
639 vstatus.cur_user = ''
642 reset_idler(v, vstatus, 10)
646 if not has_good_pin(uid):
647 logging.info('user '+vstatus.cur_user+' has a bad PIN')
648 vstatus.mk.set_messages(
649 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
650 vstatus.cur_user = ''
653 reset_idler(v, vstatus, 3)
657 if acct_is_disabled():
658 logging.info('user '+vstatus.cur_user+' is disabled')
659 vstatus.mk.set_messages(
660 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
661 vstatus.cur_user = ''
664 reset_idler(v, vstatus, 3)
669 vstatus.mk.set_message('PIN: ')
670 logging.info('need pin for user %s'%vstatus.cur_user)
671 vstatus.change_state(STATE_GETTING_PIN)
675 def handle_idle_key(state, event, params, v, vstatus):
676 #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
681 vstatus.cur_user = ''
682 reset_idler(v, vstatus)
685 vstatus.change_state(STATE_GETTING_UID)
686 run_handler(event, key, v, vstatus)
689 def handle_idle_tick(state, event, params, v, vstatus):
691 if vstatus.mk.done():
694 if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
695 vstatus.time_of_next_idler = time() + 30
700 vstatus.mk.update_display()
702 vstatus.change_state(STATE_GRANDFATHER_CLOCK)
703 run_handler(event, params, v, vstatus)
706 def beep_on(when, before=0):
707 start = int(when - before)
711 if now >= start and now <= end:
715 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
716 ### check for interesting times
719 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
720 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
721 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
722 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
724 hourfromnow = localtime(time() + 3600)
726 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
727 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
728 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
730 ## check for X seconds to the hour
731 ## if case, update counter to 2
732 if beep_on(onthehour,15) \
733 or beep_on(halfhour,0) \
734 or beep_on(quarterhour,0) \
735 or beep_on(threequarterhour,0) \
736 or beep_on(fivetothehour,0):
737 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
738 run_handler(event, params, v, vstatus)
740 vstatus.change_state(STATE_IDLE)
742 def handle_grandfather_tick(state, event, params, v, vstatus):
746 ### we live in interesting times
749 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
750 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
751 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
752 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
754 hourfromnow = localtime(time() + 3600)
756 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
757 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
758 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
761 #print "when it fashionable to wear a onion on your hip"
763 if beep_on(onthehour,15):
765 next_hour=((hourfromnow[3] + 11) % 12) + 1
766 if onthehour - time() < next_hour and onthehour - time() > 0:
771 msg.append(("DING!", False, None))
773 msg.append((" DING!", False, None))
774 elif int(onthehour - time()) == 0:
776 msg.append((" BONG!", False, None))
777 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
778 elif beep_on(halfhour,0):
781 msg.append((" HALFHOUR ", False, 50))
782 elif beep_on(quarterhour,0):
785 msg.append((" QTR HOUR ", False, 50))
786 elif beep_on(threequarterhour,0):
789 msg.append((" 3 QTR HR ", False, 50))
790 elif beep_on(fivetothehour,0):
793 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
797 ## check for X seconds to the hour
800 vstatus.mk.set_messages(msg)
803 vstatus.mk.update_display()
804 ## if no longer case, return to idle
806 ## change idler to be clock
807 if go_idle and vstatus.mk.done():
808 vstatus.change_state(STATE_IDLE,1)
810 def handle_door_idle(state, event, params, v, vstatus):
811 def twiddle(clock,v,wise = 2):
813 v.display("-FEED ME-")
814 elif (clock % 4 == 1+wise):
815 v.display("\\FEED ME/")
816 elif (clock % 4 == 2):
817 v.display("-FEED ME-")
818 elif (clock % 4 == 3-wise):
819 v.display("/FEED ME\\")
821 # don't care right now.
824 if ((now % 60 % 2) == 0):
827 twiddle(now, v, wise=0)
830 def handle_door_event(state, event, params, v, vstatus):
831 if params == 0: #door open
832 vstatus.change_state(STATE_DOOR_OPENING)
833 logging.warning("Entering open door mode")
834 v.display("-FEED ME-")
836 vstatus.cur_user = ''
838 elif params == 1: #door closed
839 vstatus.change_state(STATE_DOOR_CLOSING)
840 reset_idler(v, vstatus, 3)
842 logging.warning('Leaving open door mode')
843 v.display("-YUM YUM!-")
845 def handle_mifare_event(state, event, params, v, vstatus):
847 # Translate card_id into uid.
852 vstatus.cur_user = get_uid(card_id)
853 logging.info('Mapped card id to uid %s'%vstatus.cur_user)
854 vstatus.username = get_uname(vstatus.cur_user)
855 if acct_is_disabled(vstatus.username):
856 vstatus.username = '-disabled-'
858 vstatus.username = None
859 if vstatus.username == '-disabled-':
861 vstatus.mk.set_messages(
862 [(center('ACCT DISABLED'), False, 1.0),
863 (center('SORRY'), False, 0.5)])
864 vstatus.cur_user = ''
866 vstatus.username = None
868 reset_idler(v, vstatus, 2)
870 elif vstatus.username:
872 vstatus.cur_selection = ''
873 vstatus.change_state(STATE_GET_SELECTION)
874 scroll_options(vstatus.username, vstatus.mk, True)
878 vstatus.mk.set_messages(
879 [(center('BAD CARD'), False, 1.0),
880 (center('SORRY'), False, 0.5)])
881 vstatus.cur_user = ''
884 reset_idler(v, vstatus, 2)
887 def handle_mifare_add_user_event(state, event, params, v, vstatus):
890 # Translate card_id into uid.
895 if get_uid(card_id) != None:
896 vstatus.mk.set_messages(
897 [(center('ALREADY'), False, 0.5),
898 (center('ENROLLED'), False, 0.5)])
900 # scroll_options(vstatus.username, vstatus.mk)
905 logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
906 set_card_id(vstatus.cur_user, card_id)
907 vstatus.mk.set_messages(
908 [(center('CARD'), False, 0.5),
909 (center('ENROLLED'), False, 0.5)])
911 # scroll_options(vstatus.username, vstatus.mk)
913 def return_to_idle(state,event,params,v,vstatus):
914 reset_idler(v, vstatus)
916 def create_state_table(vstatus):
917 vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
918 vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
919 vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
920 vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
922 vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
923 vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
924 vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
925 vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
927 vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
928 vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
929 vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
930 vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
932 vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
933 vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = handle_door_event
934 vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
935 vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
937 vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
938 vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = handle_door_event
939 vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
940 vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
942 vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
943 vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = handle_door_event
944 vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
945 vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
947 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
948 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
949 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = handle_door_event
950 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = handle_door_event
951 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
952 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
953 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
955 def get_state_table_handler(vstatus, state, event, counter):
956 return vstatus.state_table[(state,event,counter)]
958 def time_to_next_update(vstatus):
959 idle_update = vstatus.time_of_next_idlestep - time()
960 if not vstatus.mk.done() and vstatus.mk.next_update is not None:
961 mk_update = vstatus.mk.next_update - time()
962 if mk_update < idle_update:
963 idle_update = mk_update
966 def run_forever(rfh, wfh, options, cf):
967 v = VendingMachine(rfh, wfh, USE_MIFARE)
968 vstatus = VendState(v)
969 create_state_table(vstatus)
971 logging.debug('PING is ' + str(v.ping()))
973 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
976 reset_idler(v, vstatus)
978 # This main loop was hideous and the work of the devil.
979 # This has now been fixed (mostly) - mtearle
982 # notes for later surgery
983 # (event, counter, ' ')
987 # ( return state - not currently implemented )
993 except DispenseDatabaseException, e:
994 logging.error('Database error: '+str(e))
996 timeout = time_to_next_update(vstatus)
997 e = v.next_event(timeout)
1000 run_handler(event, params, v, vstatus)
1002 # logging.debug('Got event: ' + repr(e))
1005 def run_handler(event, params, v, vstatus):
1006 handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
1008 handler(vstatus.state, event, params, v, vstatus)
1010 def connect_to_vend(options, cf):
1013 logging.info('Connecting to vending machine using LAT')
1014 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
1015 rfh, wfh = latclient.get_fh()
1016 elif options.use_serial:
1017 # Open vending machine via serial.
1018 logging.info('Connecting to vending machine using serial')
1019 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
1020 rfh,wfh = serialclient.get_fh()
1022 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
1023 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
1025 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
1026 sock.connect((options.host, options.port))
1027 rfh = sock.makefile('r')
1028 wfh = sock.makefile('w')
1035 from optparse import OptionParser
1037 op = OptionParser(usage="%prog [OPTION]...")
1038 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')
1039 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
1040 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
1041 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
1042 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
1043 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
1044 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
1045 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
1046 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
1047 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
1048 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
1049 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
1050 options, args = op.parse_args()
1053 op.error('extra command line arguments: ' + ' '.join(args))
1058 'DBServer': ('Database', 'Server'),
1059 'DBName': ('Database', 'Name'),
1060 'DBUser': ('VendingMachine', 'DBUser'),
1061 'DBPassword': ('VendingMachine', 'DBPassword'),
1063 'ServiceName': ('VendingMachine', 'ServiceName'),
1064 'ServicePassword': ('VendingMachine', 'Password'),
1066 'ServerName': ('DecServer', 'Name'),
1067 'ConnectPassword': ('DecServer', 'ConnectPassword'),
1068 'PrivPassword': ('DecServer', 'PrivPassword'),
1071 class VendConfigFile:
1072 def __init__(self, config_file, options):
1074 cp = ConfigParser.ConfigParser()
1075 cp.read(config_file)
1077 for option in options:
1078 section, name = options[option]
1079 value = cp.get(section, name)
1080 self.__dict__[option] = value
1082 except ConfigParser.Error, e:
1083 raise SystemExit("Error reading config file "+config_file+": " + str(e))
1085 def create_pid_file(name):
1087 pid_file = file(name, 'w')
1088 pid_file.write('%d\n'%os.getpid())
1091 logging.warning('unable to write to pid file '+name+': '+str(e))
1094 def do_nothing(signum, stack):
1095 signal.signal(signum, do_nothing)
1096 def stop_server(signum, stack): raise KeyboardInterrupt
1097 signal.signal(signal.SIGHUP, do_nothing)
1098 signal.signal(signal.SIGTERM, stop_server)
1099 signal.signal(signal.SIGINT, stop_server)
1101 options = parse_args()
1102 config_opts = VendConfigFile(options.config_file, config_options)
1103 if options.daemon: become_daemon()
1104 set_up_logging(options)
1105 if options.pid_file != '': create_pid_file(options.pid_file)
1107 return options, config_opts
1109 def clean_up_nicely(options, config_opts):
1110 if options.pid_file != '':
1112 os.unlink(options.pid_file)
1113 logging.debug('Removed pid file '+options.pid_file)
1114 except OSError: pass # if we can't delete it, meh
1116 def set_up_logging(options):
1117 logger = logging.getLogger()
1119 if not options.daemon:
1120 stderr_logger = logging.StreamHandler(sys.stderr)
1121 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1122 logger.addHandler(stderr_logger)
1124 if options.log_file != '':
1126 file_logger = logging.FileHandler(options.log_file)
1127 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1128 logger.addHandler(file_logger)
1130 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1132 if options.syslog != None:
1133 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1134 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1135 logger.addHandler(sys_logger)
1138 logger.setLevel(logging.WARNING)
1139 elif options.verbose:
1140 logger.setLevel(logging.DEBUG)
1142 logger.setLevel(logging.INFO)
1144 def become_daemon():
1145 dev_null = file('/dev/null')
1146 fd = dev_null.fileno()
1155 raise SystemExit('failed to fork: '+str(e))
1157 def do_vend_server(options, config_opts):
1160 rfh, wfh = connect_to_vend(options, config_opts)
1161 except (SerialClientException, socket.error), e:
1162 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1164 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1165 logging.info("Trying again in 5 seconds.")
1169 # run_forever(rfh, wfh, options, config_opts)
1172 run_forever(rfh, wfh, options, config_opts)
1173 except VendingException:
1174 logging.error("Connection died, trying again...")
1175 logging.info("Trying again in 5 seconds.")
1178 if __name__ == '__main__':
1179 options, config_opts = set_stuff_up()
1182 logging.warning('Starting Vend Server')
1183 do_vend_server(options, config_opts)
1184 logging.error('Vend Server finished unexpectedly, restarting')
1185 except KeyboardInterrupt:
1186 logging.info("Killed by signal, cleaning up")
1187 clean_up_nicely(options, config_opts)
1188 logging.warning("Vend Server stopped")
1193 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1194 tb = format_tb(exc_traceback, 20)
1197 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1198 logging.critical("Message: " + str(exc_value))
1199 logging.critical("Traceback:")
1201 for line in event.split('\n'):
1202 logging.critical(' '+line)
1203 logging.critical("This message should be considered a bug in the Vend Server.")
1204 logging.critical("Please report this to someone who can fix it.")
1206 logging.warning("Trying again anyway (might not help, but hey...)")