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
257 GrayIdler(v,one="*",zero="-"),
258 GrayIdler(v,one="/",zero="\\"),
259 GrayIdler(v,one="X",zero="O"),
260 GrayIdler(v,one="*",zero="-",reorder=1),
261 GrayIdler(v,one="/",zero="\\",reorder=1),
262 GrayIdler(v,one="X",zero="O",reorder=1),
268 StringIdler(v), # Hello Cruel World
269 StringIdler(v, text="Kill 'em all", repeat=False),
270 StringIdler(v, text=CREDITS),
271 StringIdler(v, text=str(math.pi) + " "),
272 StringIdler(v, text=str(math.e) + " "),
273 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),
274 # "Hello World" in brainfuck
275 StringIdler(v, text=">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++."),
279 FileIdler(v, '/usr/share/common-licenses/GPL-2',affinity=2),
281 PipeIdler(v, "/usr/bin/getent", "passwd"),
282 FortuneIdler(v,affinity=20),
287 def reset_idler(v, vstatus, t = None):
289 idler = GreetingIdler(v, t)
290 vstatus.time_of_next_idlestep = time()+idler.next()
291 vstatus.time_of_next_idler = None
292 vstatus.time_to_autologout = None
293 vstatus.change_state(STATE_IDLE, 1)
298 # Implementation of the King Of the Hill algorithm from;
299 # http://eli.thegreenplace.net/2010/01/22/weighted-random-generation-in-python/
301 #def weighted_choice_king(weights):
304 # for i, w in enumerate(weights):
306 # if random.random() * total < w:
314 for choice in idlers:
315 weight = choice.affinity()
317 if random() * total < weight:
325 def idle_step(vstatus):
329 vstatus.time_of_next_idler = time() + 30
330 nextidle = idler.next()
332 nextidle = IDLE_SPEED
333 vstatus.time_of_next_idlestep = time()+nextidle
336 def __init__(self,v):
337 self.state_table = {}
338 self.state = STATE_IDLE
341 self.mk = MessageKeeper(v)
345 self.cur_selection = ''
346 self.time_to_autologout = None
348 self.last_timeout_refresh = None
350 def change_state(self,newstate,newcounter=None):
351 if self.state != newstate:
352 #print "Changing state from: ",
356 self.state = newstate
358 if newcounter is not None and self.counter != newcounter:
359 #print "Changing counter from: ",
363 self.counter = newcounter
367 def handle_tick_event(event, params, v, vstatus):
368 # don't care right now.
371 def handle_switch_event(event, params, v, vstatus):
372 # don't care right now.
376 def do_nothing(state, event, params, v, vstatus):
377 print "doing nothing (s,e,p)", state, " ", event, " ", params
380 def handle_getting_uid_idle(state, event, params, v, vstatus):
381 # don't care right now.
384 def handle_getting_pin_idle(state, event, params, v, vstatus):
385 # don't care right now.
388 def handle_get_selection_idle(state, event, params, v, vstatus):
389 # don't care right now.
391 ### State logging out ..
392 if vstatus.time_to_autologout != None:
393 time_left = vstatus.time_to_autologout - time()
394 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
395 vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
396 vstatus.last_timeout_refresh = int(time_left)
397 vstatus.cur_selection = ''
399 if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
400 vstatus.time_to_autologout = None
401 vstatus.cur_user = ''
403 vstatus.cur_selection = ''
405 reset_idler(v, vstatus)
407 ### State fully logged out ... reset variables
408 if vstatus.time_to_autologout and not vstatus.mk.done():
409 vstatus.time_to_autologout = None
410 if vstatus.cur_user == '' and vstatus.time_to_autologout:
411 vstatus.time_to_autologout = None
414 if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
416 vstatus.time_to_autologout = time() + 15
417 vstatus.last_timeout_refresh = None
419 ## FIXME - this may need to be elsewhere.....
421 vstatus.mk.update_display()
425 def handle_get_selection_key(state, event, params, v, vstatus):
427 if len(vstatus.cur_selection) == 0:
430 vstatus.cur_user = ''
431 vstatus.cur_selection = ''
433 vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
434 reset_idler(v, vstatus, 2)
436 vstatus.cur_selection += chr(key + ord('0'))
437 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
438 vstatus.time_to_autologout = None
439 elif len(vstatus.cur_selection) == 1:
441 vstatus.cur_selection = ''
442 vstatus.time_to_autologout = None
443 scroll_options(vstatus.username, vstatus.mk)
446 vstatus.cur_selection += chr(key + ord('0'))
448 make_selection(v,vstatus)
449 vstatus.cur_selection = ''
450 vstatus.time_to_autologout = time() + 8
451 vstatus.last_timeout_refresh = None
454 price_check(v,vstatus)
455 vstatus.cur_selection = ''
456 vstatus.time_to_autologout = None
457 vstatus.last_timeout_refresh = None
459 def make_selection(v, vstatus):
460 # should use sudo here
461 if vstatus.cur_selection == '55':
462 vstatus.mk.set_message('OPENSESAME')
463 logging.info('dispensing a door for %s'%vstatus.username)
465 #ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
466 ret = os.system('dispense -u "%s" door'%vstatus.username)
468 ret = os.system('dispense door')
470 logging.info('door opened')
471 vstatus.mk.set_message(center('DOOR OPEN'))
473 logging.warning('user %s tried to dispense a bad door'%vstatus.username)
474 vstatus.mk.set_message(center('BAD DOOR'))
476 elif vstatus.cur_selection == '81':
478 elif vstatus.cur_selection == '99':
479 scroll_options(vstatus.username, vstatus.mk)
480 vstatus.cur_selection = ''
482 elif vstatus.cur_selection[1] == '8':
483 v.display('GOT DRINK?')
484 if ((os.system('dispense -u "%s" coke:%s'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
485 v.display('SEEMS NOT')
487 v.display('GOT DRINK!')
489 # first see if it's a named slot
491 price, shortname, name = get_snack( vstatus.cur_selection )
493 price, shortname, name = get_snack( '--' )
494 dollarprice = "$%.2f" % ( price / 100.0 )
495 v.display(vstatus.cur_selection+' - %s'%dollarprice)
496 # exitcode = os.system('dispense -u "%s" give \>snacksales %d "%s"'%(vstatus.username, price, name)) >> 8
497 # exitcode = os.system('dispense -u "%s" give \>sales\:snack %d "%s"'%(vstatus.username, price, name)) >> 8
498 exitcode = os.system('dispense -u "%s" snack:%s'%(vstatus.username, vstatus.cur_selection)) >> 8
500 # magic dispense syslog service
501 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
502 (worked, code, string) = v.vend(vstatus.cur_selection)
504 v.display('THANK YOU')
506 print "Vend Failed:", code, string
507 v.display('VEND FAIL')
508 elif (exitcode == 5): # RV_BALANCE
509 v.display('NO MONEY?')
510 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
511 v.display('EMPTY SLOT')
512 elif (exitcode == 1): # RV_BADITEM (Dead slot)
513 v.display('EMPTY SLOT')
515 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
516 v.display('UNK ERROR')
520 def price_check(v, vstatus):
521 if vstatus.cur_selection[1] == '8':
522 args = ('dispense', 'iteminfo', 'coke:' + vstatus.cur_selection[0])
523 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
524 dollarprice = re.match("\s*[a-z]+:\d+\s+(\d+\.\d\d)\s+([^\n]+)", info).group(1)
526 # first see if it's a named slot
528 price, shortname, name = get_snack( vstatus.cur_selection )
530 price, shortname, name = get_snack( '--' )
531 dollarprice = "$%.2f" % ( price / 100.0 )
532 v.display(vstatus.cur_selection+' - %s'%dollarprice)
535 def handle_getting_pin_key(state, event, params, v, vstatus):
536 #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
538 if len(vstatus.cur_pin) < PIN_LENGTH:
540 if vstatus.cur_pin == '':
541 vstatus.cur_user = ''
542 reset_idler(v, vstatus)
546 vstatus.mk.set_message('PIN: ')
548 vstatus.cur_pin += chr(key + ord('0'))
549 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
550 if len(vstatus.cur_pin) == PIN_LENGTH:
551 vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
554 vstatus.cur_selection = ''
555 vstatus.change_state(STATE_GET_SELECTION)
556 scroll_options(vstatus.username, vstatus.mk, True)
560 vstatus.mk.set_messages(
561 [(center('BAD PIN'), False, 1.0),
562 (center('SORRY'), False, 0.5)])
563 vstatus.cur_user = ''
566 reset_idler(v, vstatus, 2)
571 def handle_getting_uid_key(state, event, params, v, vstatus):
572 #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
575 # complicated key handling here:
577 if len(vstatus.cur_user) == 0 and key == 9:
578 vstatus.cur_selection = ''
579 vstatus.time_to_autologout = None
580 vstatus.mk.set_message('PRICECHECK')
582 scroll_options('', vstatus.mk)
583 vstatus.change_state(STATE_GET_SELECTION)
586 if len(vstatus.cur_user) <8:
588 vstatus.cur_user = ''
590 reset_idler(v, vstatus)
592 vstatus.cur_user += chr(key + ord('0'))
593 #logging.info('dob: '+vstatus.cur_user)
594 if len(vstatus.cur_user) > 5:
595 vstatus.mk.set_message('>'+vstatus.cur_user)
597 vstatus.mk.set_message('UID: '+vstatus.cur_user)
599 if len(vstatus.cur_user) == 5:
600 uid = int(vstatus.cur_user)
603 logging.info('user '+vstatus.cur_user+' has a bad PIN')
609 Welcome to Picklevision Sytems, Sunnyvale, CA
611 Greetings Professor Falken.
616 Shall we play a game?
619 Please choose from the following menu:
626 6. Toxic and Biochemical Warfare
627 7. Global Thermonuclear War
631 Wouldn't you prefer a nice game of chess?
633 """.replace('\n',' ')
634 vstatus.mk.set_messages([(pfalken, False, 10)])
635 vstatus.cur_user = ''
638 reset_idler(v, vstatus, 10)
642 if not has_good_pin(uid):
643 logging.info('user '+vstatus.cur_user+' has a bad PIN')
644 vstatus.mk.set_messages(
645 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
646 vstatus.cur_user = ''
649 reset_idler(v, vstatus, 3)
653 if acct_is_disabled():
654 logging.info('user '+vstatus.cur_user+' is disabled')
655 vstatus.mk.set_messages(
656 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
657 vstatus.cur_user = ''
660 reset_idler(v, vstatus, 3)
665 vstatus.mk.set_message('PIN: ')
666 logging.info('need pin for user %s'%vstatus.cur_user)
667 vstatus.change_state(STATE_GETTING_PIN)
671 def handle_idle_key(state, event, params, v, vstatus):
672 #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
677 vstatus.cur_user = ''
678 reset_idler(v, vstatus)
681 vstatus.change_state(STATE_GETTING_UID)
682 run_handler(event, key, v, vstatus)
685 def handle_idle_tick(state, event, params, v, vstatus):
687 if vstatus.mk.done():
690 if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
691 vstatus.time_of_next_idler = time() + 30
696 vstatus.mk.update_display()
698 vstatus.change_state(STATE_GRANDFATHER_CLOCK)
699 run_handler(event, params, v, vstatus)
702 def beep_on(when, before=0):
703 start = int(when - before)
707 if now >= start and now <= end:
711 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
712 ### check for interesting times
715 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
716 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
717 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
718 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
720 hourfromnow = localtime(time() + 3600)
722 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
723 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
724 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
726 ## check for X seconds to the hour
727 ## if case, update counter to 2
728 if beep_on(onthehour,15) \
729 or beep_on(halfhour,0) \
730 or beep_on(quarterhour,0) \
731 or beep_on(threequarterhour,0) \
732 or beep_on(fivetothehour,0):
733 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
734 run_handler(event, params, v, vstatus)
736 vstatus.change_state(STATE_IDLE)
738 def handle_grandfather_tick(state, event, params, v, vstatus):
742 ### we live in interesting times
745 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
746 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
747 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
748 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
750 hourfromnow = localtime(time() + 3600)
752 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
753 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
754 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
757 #print "when it fashionable to wear a onion on your hip"
759 if beep_on(onthehour,15):
761 next_hour=((hourfromnow[3] + 11) % 12) + 1
762 if onthehour - time() < next_hour and onthehour - time() > 0:
767 msg.append(("DING!", False, None))
769 msg.append((" DING!", False, None))
770 elif int(onthehour - time()) == 0:
772 msg.append((" BONG!", False, None))
773 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
774 elif beep_on(halfhour,0):
777 msg.append((" HALFHOUR ", False, 50))
778 elif beep_on(quarterhour,0):
781 msg.append((" QTR HOUR ", False, 50))
782 elif beep_on(threequarterhour,0):
785 msg.append((" 3 QTR HR ", False, 50))
786 elif beep_on(fivetothehour,0):
789 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
793 ## check for X seconds to the hour
796 vstatus.mk.set_messages(msg)
799 vstatus.mk.update_display()
800 ## if no longer case, return to idle
802 ## change idler to be clock
803 if go_idle and vstatus.mk.done():
804 vstatus.change_state(STATE_IDLE,1)
806 def handle_door_idle(state, event, params, v, vstatus):
807 def twiddle(clock,v,wise = 2):
809 v.display("-FEED ME-")
810 elif (clock % 4 == 1+wise):
811 v.display("\\FEED ME/")
812 elif (clock % 4 == 2):
813 v.display("-FEED ME-")
814 elif (clock % 4 == 3-wise):
815 v.display("/FEED ME\\")
817 # don't care right now.
820 if ((now % 60 % 2) == 0):
823 twiddle(now, v, wise=0)
826 def handle_door_event(state, event, params, v, vstatus):
827 if params == 0: #door open
828 vstatus.change_state(STATE_DOOR_OPENING)
829 logging.warning("Entering open door mode")
830 v.display("-FEED ME-")
832 vstatus.cur_user = ''
834 elif params == 1: #door closed
835 vstatus.change_state(STATE_DOOR_CLOSING)
836 reset_idler(v, vstatus, 3)
838 logging.warning('Leaving open door mode')
839 v.display("-YUM YUM!-")
841 def handle_mifare_event(state, event, params, v, vstatus):
843 # Translate card_id into uid.
848 vstatus.cur_user = get_uid(card_id)
849 logging.info('Mapped card id to uid %s'%vstatus.cur_user)
850 vstatus.username = get_uname(vstatus.cur_user)
851 if acct_is_disabled(vstatus.username):
852 vstatus.username = '-disabled-'
854 vstatus.username = None
855 if vstatus.username == '-disabled-':
857 vstatus.mk.set_messages(
858 [(center('ACCT DISABLED'), False, 1.0),
859 (center('SORRY'), False, 0.5)])
860 vstatus.cur_user = ''
862 vstatus.username = None
864 reset_idler(v, vstatus, 2)
866 elif vstatus.username:
868 vstatus.cur_selection = ''
869 vstatus.change_state(STATE_GET_SELECTION)
870 scroll_options(vstatus.username, vstatus.mk, True)
874 vstatus.mk.set_messages(
875 [(center('BAD CARD'), False, 1.0),
876 (center('SORRY'), False, 0.5)])
877 vstatus.cur_user = ''
880 reset_idler(v, vstatus, 2)
883 def handle_mifare_add_user_event(state, event, params, v, vstatus):
886 # Translate card_id into uid.
891 if get_uid(card_id) != None:
892 vstatus.mk.set_messages(
893 [(center('ALREADY'), False, 0.5),
894 (center('ENROLLED'), False, 0.5)])
896 # scroll_options(vstatus.username, vstatus.mk)
901 logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
902 set_card_id(vstatus.cur_user, card_id)
903 vstatus.mk.set_messages(
904 [(center('CARD'), False, 0.5),
905 (center('ENROLLED'), False, 0.5)])
907 # scroll_options(vstatus.username, vstatus.mk)
909 def return_to_idle(state,event,params,v,vstatus):
910 reset_idler(v, vstatus)
912 def create_state_table(vstatus):
913 vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
914 vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
915 vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
916 vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
918 vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
919 vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
920 vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
921 vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
923 vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
924 vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
925 vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
926 vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
928 vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
929 vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
930 vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
931 vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
933 vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
934 vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
935 vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
936 vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
938 vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
939 vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
940 vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
941 vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
943 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
944 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
945 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
946 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
947 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
948 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
949 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
951 def get_state_table_handler(vstatus, state, event, counter):
952 return vstatus.state_table[(state,event,counter)]
954 def time_to_next_update(vstatus):
955 idle_update = vstatus.time_of_next_idlestep - time()
956 if not vstatus.mk.done() and vstatus.mk.next_update is not None:
957 mk_update = vstatus.mk.next_update - time()
958 if mk_update < idle_update:
959 idle_update = mk_update
962 def run_forever(rfh, wfh, options, cf):
963 v = VendingMachine(rfh, wfh, USE_MIFARE)
964 vstatus = VendState(v)
965 create_state_table(vstatus)
967 logging.debug('PING is ' + str(v.ping()))
969 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
972 reset_idler(v, vstatus)
974 # This main loop was hideous and the work of the devil.
975 # This has now been fixed (mostly) - mtearle
978 # notes for later surgery
979 # (event, counter, ' ')
983 # ( return state - not currently implemented )
989 except DispenseDatabaseException, e:
990 logging.error('Database error: '+str(e))
992 timeout = time_to_next_update(vstatus)
993 e = v.next_event(timeout)
996 run_handler(event, params, v, vstatus)
998 # logging.debug('Got event: ' + repr(e))
1001 def run_handler(event, params, v, vstatus):
1002 handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
1004 handler(vstatus.state, event, params, v, vstatus)
1006 def connect_to_vend(options, cf):
1009 logging.info('Connecting to vending machine using LAT')
1010 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
1011 rfh, wfh = latclient.get_fh()
1012 elif options.use_serial:
1013 # Open vending machine via serial.
1014 logging.info('Connecting to vending machine using serial')
1015 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
1016 rfh,wfh = serialclient.get_fh()
1018 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
1019 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
1021 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
1022 sock.connect((options.host, options.port))
1023 rfh = sock.makefile('r')
1024 wfh = sock.makefile('w')
1031 from optparse import OptionParser
1033 op = OptionParser(usage="%prog [OPTION]...")
1034 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')
1035 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
1036 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
1037 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
1038 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
1039 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
1040 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
1041 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
1042 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
1043 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
1044 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
1045 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
1046 options, args = op.parse_args()
1049 op.error('extra command line arguments: ' + ' '.join(args))
1054 'DBServer': ('Database', 'Server'),
1055 'DBName': ('Database', 'Name'),
1056 'DBUser': ('VendingMachine', 'DBUser'),
1057 'DBPassword': ('VendingMachine', 'DBPassword'),
1059 'ServiceName': ('VendingMachine', 'ServiceName'),
1060 'ServicePassword': ('VendingMachine', 'Password'),
1062 'ServerName': ('DecServer', 'Name'),
1063 'ConnectPassword': ('DecServer', 'ConnectPassword'),
1064 'PrivPassword': ('DecServer', 'PrivPassword'),
1067 class VendConfigFile:
1068 def __init__(self, config_file, options):
1070 cp = ConfigParser.ConfigParser()
1071 cp.read(config_file)
1073 for option in options:
1074 section, name = options[option]
1075 value = cp.get(section, name)
1076 self.__dict__[option] = value
1078 except ConfigParser.Error, e:
1079 raise SystemExit("Error reading config file "+config_file+": " + str(e))
1081 def create_pid_file(name):
1083 pid_file = file(name, 'w')
1084 pid_file.write('%d\n'%os.getpid())
1087 logging.warning('unable to write to pid file '+name+': '+str(e))
1090 def do_nothing(signum, stack):
1091 signal.signal(signum, do_nothing)
1092 def stop_server(signum, stack): raise KeyboardInterrupt
1093 signal.signal(signal.SIGHUP, do_nothing)
1094 signal.signal(signal.SIGTERM, stop_server)
1095 signal.signal(signal.SIGINT, stop_server)
1097 options = parse_args()
1098 config_opts = VendConfigFile(options.config_file, config_options)
1099 if options.daemon: become_daemon()
1100 set_up_logging(options)
1101 if options.pid_file != '': create_pid_file(options.pid_file)
1103 return options, config_opts
1105 def clean_up_nicely(options, config_opts):
1106 if options.pid_file != '':
1108 os.unlink(options.pid_file)
1109 logging.debug('Removed pid file '+options.pid_file)
1110 except OSError: pass # if we can't delete it, meh
1112 def set_up_logging(options):
1113 logger = logging.getLogger()
1115 if not options.daemon:
1116 stderr_logger = logging.StreamHandler(sys.stderr)
1117 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1118 logger.addHandler(stderr_logger)
1120 if options.log_file != '':
1122 file_logger = logging.FileHandler(options.log_file)
1123 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1124 logger.addHandler(file_logger)
1126 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1128 if options.syslog != None:
1129 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1130 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1131 logger.addHandler(sys_logger)
1134 logger.setLevel(logging.WARNING)
1135 elif options.verbose:
1136 logger.setLevel(logging.DEBUG)
1138 logger.setLevel(logging.INFO)
1140 def become_daemon():
1141 dev_null = file('/dev/null')
1142 fd = dev_null.fileno()
1151 raise SystemExit('failed to fork: '+str(e))
1153 def do_vend_server(options, config_opts):
1156 rfh, wfh = connect_to_vend(options, config_opts)
1157 except (SerialClientException, socket.error), e:
1158 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1160 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1161 logging.info("Trying again in 5 seconds.")
1165 # run_forever(rfh, wfh, options, config_opts)
1168 run_forever(rfh, wfh, options, config_opts)
1169 except VendingException:
1170 logging.error("Connection died, trying again...")
1171 logging.info("Trying again in 5 seconds.")
1174 if __name__ == '__main__':
1175 options, config_opts = set_stuff_up()
1178 logging.warning('Starting Vend Server')
1179 do_vend_server(options, config_opts)
1180 logging.error('Vend Server finished unexpectedly, restarting')
1181 except KeyboardInterrupt:
1182 logging.info("Killed by signal, cleaning up")
1183 clean_up_nicely(options, config_opts)
1184 logging.warning("Vend Server stopped")
1189 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1190 tb = format_tb(exc_traceback, 20)
1193 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1194 logging.critical("Message: " + str(exc_value))
1195 logging.critical("Traceback:")
1197 for line in event.split('\n'):
1198 logging.critical(' '+line)
1199 logging.critical("This message should be considered a bug in the Vend Server.")
1200 logging.critical("Please report this to someone who can fix it.")
1202 logging.warning("Trying again anyway (might not help, but hey...)")