8 import sys, os, string, re, pwd, signal, math, syslog
9 import logging, logging.handlers
10 from traceback import format_tb
12 from time import time, sleep, mktime, localtime
13 from subprocess import Popen, PIPE
14 from LATClient import LATClient, LATClientException
15 from SerialClient import SerialClient, SerialClientException
16 from VendingMachine import VendingMachine, VendingException
17 from MessageKeeper import MessageKeeper
18 from HorizScroll import HorizScroll
19 from random import random, seed
20 from Idler import GreetingIdler,TrainIdler,GrayIdler,StringIdler,ClockIdler,FortuneIdler,FileIdler,PipeIdler
21 from SnackConfig import get_snack#, get_snacks
23 from posix import geteuid
24 from LDAPConnector import get_uid,get_uname, set_card_id
27 This vending machine software brought to you by:
32 and a collective of hungry alpacas.
36 For a good time call +61 8 6488 3901
58 STATE_GRANDFATHER_CLOCK,
64 class DispenseDatabaseException(Exception): pass
66 class DispenseDatabase:
67 def __init__(self, vending_machine, host, name, user, password):
68 self.vending_machine = vending_machine
69 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
70 self.db.query('LISTEN vend_requests')
72 def process_requests(self):
73 logging.debug('database processing')
74 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
76 outstanding = self.db.query(query).getresult()
77 except (pg.error,), db_err:
78 raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
79 for (id, slot) in outstanding:
80 (worked, code, string) = self.vending_machine.vend(slot)
81 logging.debug (str((worked, code, string)))
83 query = 'SELECT vend_success(%s)'%id
84 self.db.query(query).getresult()
86 query = 'SELECT vend_failed(%s)'%id
87 self.db.query(query).getresult()
89 def handle_events(self):
90 notifier = self.db.getnotify()
91 while notifier is not None:
92 self.process_requests()
93 notify = self.db.getnotify()
95 def scroll_options(username, mk, welcome = False):
98 acct, unused = Popen(['dispense', 'acct', username], close_fds=True, stdout=PIPE).communicate()
99 # this is fucking appalling
100 balance = acct[acct.find("$")+1:acct.find("(")].strip()
102 msg = [(center('WELCOME'), False, TEXT_SPEED),
103 (center(username), False, TEXT_SPEED),
104 (center(balance), False, TEXT_SPEED),]
107 choices = ' '*10+'CHOICES: '
111 for i in range(0, 7):
112 args = ('dispense', 'iteminfo', 'coke:%i' % i)
113 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
114 m = re.match("\s*[a-z]+:\d+\s+(\d+)\.(\d\d)\s+([^\n]+)", info)
115 cents = int(m.group(1))*100 + int(m.group(2))
116 cokes.append('%i %i %s' % (i, cents, m.group(3)));
120 (slot_num, price, slot_name) = c.split(' ', 2)
121 if slot_name == 'dead': continue
122 choices += '%s-(%sc)-%s8 '%(slot_name, price, slot_num)
124 # we don't want to print snacks for now since it'll be too large
125 # and there's physical bits of paper in the machine anyway - matt
127 # snacks = get_snacks()
131 # for slot, ( name, price ) in snacks.items():
132 # choices += '%s8-%s (%sc) ' % ( slot, name, price )
134 choices += '55-DOOR '
135 choices += 'OR ANOTHER SNACK. '
136 choices += '99 TO READ AGAIN. '
137 choices += 'CHOICE? '
138 msg.append((choices, False, None))
145 def _check_pin(uid, pin):
149 print "_check_pin('",uid,"',---)"
152 info = pwd.getpwuid(uid)
154 logging.info('getting pin for uid %d: user not in password file'%uid)
156 if info.pw_dir == None: return False
157 pinfile = os.path.join(info.pw_dir, '.pin')
161 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
164 logging.info('getting pin for uid %d: .pin has wrong permissions. Fixing.'%uid)
165 os.chmod(pinfile, 0600)
169 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
171 pinstr = f.readline()
173 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
174 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
178 _pin_uname = info.pw_name
181 if pin == int(pinstr):
182 logging.info("Pin correct for %d",uid)
184 logging.info("Pin incorrect for %d",uid)
185 return pin == int(pinstr)
187 def acct_is_disabled(name=None):
191 acct, unused = Popen(['dispense', 'acct', _pin_uname], close_fds=True, stdout=PIPE).communicate()
192 # this is fucking appalling
193 flags = acct[acct.find("(")+1:acct.find(")")].strip()
194 if 'disabled' in flags:
196 if 'internal' in flags:
200 def has_good_pin(uid):
201 return _check_pin(uid, None) != None
203 def verify_user_pin(uid, pin, skip_pin_check=False):
204 if skip_pin_check or _check_pin(uid, pin) == True:
205 info = pwd.getpwuid(uid)
207 if acct_is_disabled(info.pw_name):
208 logging.info('refused mifare for disabled acct uid %d (%s)'%(uid,info.pw_name))
210 logging.info('accepted mifare for uid %d (%s)'%(uid,info.pw_name))
212 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
215 logging.info('refused pin for uid %d'%(uid))
221 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
222 choice = int(random()*len(messages))
223 msg = messages[choice]
224 left = range(len(msg))
225 for i in range(len(msg)):
226 if msg[i] == ' ': left.remove(i)
230 for i in range(0, len(msg)):
236 s += chr(int(random()*26)+ord('A'))
245 return ' '*((LEN-len(str))/2)+str
256 StringIdler(v, text="Kill 'em all", repeat=False),
257 GrayIdler(v,one="*",zero="-"),
258 StringIdler(v, text=CREDITS),
259 GrayIdler(v,one="/",zero="\\"),
261 GrayIdler(v,one="X",zero="O"),
262 FileIdler(v, '/usr/share/common-licenses/GPL-2',affinity=2),
263 GrayIdler(v,one="*",zero="-",reorder=1),
264 StringIdler(v, text=str(math.pi) + " "),
266 GrayIdler(v,one="/",zero="\\",reorder=1),
267 StringIdler(v, text=str(math.e) + " "),
268 GrayIdler(v,one="X",zero="O",reorder=1),
269 StringIdler(v, text=" I want some pizza - please call Pizza Hut Shenton Park on +61 8 9381 9979 [now closed? - MSH] - and order as Quinn - I am getting really hungry", repeat=False),
270 PipeIdler(v, "/usr/bin/getent", "passwd"),
275 # "Hello World" in brainfuck
276 StringIdler(v, text=">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++."),
281 def reset_idler(v, vstatus, t = None):
283 idler = GreetingIdler(v, t)
284 vstatus.time_of_next_idlestep = time()+idler.next()
285 vstatus.time_of_next_idler = None
286 vstatus.time_to_autologout = None
287 vstatus.change_state(STATE_IDLE, 1)
292 average_affinity = 10 # guessing here...
294 if idler and idler.__class__ != GreetingIdler:
295 iiindex = idlers.index(idler)
299 move = int(random()*len(idlers)*average_affinity) + 1
304 idler = idlers[iiindex]
305 move -= idler.affinity()
309 def idle_step(vstatus):
313 vstatus.time_of_next_idler = time() + 30
314 nextidle = idler.next()
316 nextidle = IDLE_SPEED
317 vstatus.time_of_next_idlestep = time()+nextidle
320 def __init__(self,v):
321 self.state_table = {}
322 self.state = STATE_IDLE
325 self.mk = MessageKeeper(v)
329 self.cur_selection = ''
330 self.time_to_autologout = None
332 self.last_timeout_refresh = None
334 def change_state(self,newstate,newcounter=None):
335 if self.state != newstate:
336 #print "Changing state from: ",
340 self.state = newstate
342 if newcounter is not None and self.counter != newcounter:
343 #print "Changing counter from: ",
347 self.counter = newcounter
351 def handle_tick_event(event, params, v, vstatus):
352 # don't care right now.
355 def handle_switch_event(event, params, v, vstatus):
356 # don't care right now.
360 def do_nothing(state, event, params, v, vstatus):
361 print "doing nothing (s,e,p)", state, " ", event, " ", params
364 def handle_getting_uid_idle(state, event, params, v, vstatus):
365 # don't care right now.
368 def handle_getting_pin_idle(state, event, params, v, vstatus):
369 # don't care right now.
372 def handle_get_selection_idle(state, event, params, v, vstatus):
373 # don't care right now.
375 ### State logging out ..
376 if vstatus.time_to_autologout != None:
377 time_left = vstatus.time_to_autologout - time()
378 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
379 vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
380 vstatus.last_timeout_refresh = int(time_left)
381 vstatus.cur_selection = ''
383 if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
384 vstatus.time_to_autologout = None
385 vstatus.cur_user = ''
387 vstatus.cur_selection = ''
389 reset_idler(v, vstatus)
391 ### State fully logged out ... reset variables
392 if vstatus.time_to_autologout and not vstatus.mk.done():
393 vstatus.time_to_autologout = None
394 if vstatus.cur_user == '' and vstatus.time_to_autologout:
395 vstatus.time_to_autologout = None
398 if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
400 vstatus.time_to_autologout = time() + 15
401 vstatus.last_timeout_refresh = None
403 ## FIXME - this may need to be elsewhere.....
405 vstatus.mk.update_display()
409 def handle_get_selection_key(state, event, params, v, vstatus):
411 if len(vstatus.cur_selection) == 0:
414 vstatus.cur_user = ''
415 vstatus.cur_selection = ''
417 vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
418 reset_idler(v, vstatus, 2)
420 vstatus.cur_selection += chr(key + ord('0'))
421 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
422 vstatus.time_to_autologout = None
423 elif len(vstatus.cur_selection) == 1:
425 vstatus.cur_selection = ''
426 vstatus.time_to_autologout = None
427 scroll_options(vstatus.username, vstatus.mk)
430 vstatus.cur_selection += chr(key + ord('0'))
432 make_selection(v,vstatus)
433 vstatus.cur_selection = ''
434 vstatus.time_to_autologout = time() + 8
435 vstatus.last_timeout_refresh = None
438 price_check(v,vstatus)
439 vstatus.cur_selection = ''
440 vstatus.time_to_autologout = None
441 vstatus.last_timeout_refresh = None
443 def make_selection(v, vstatus):
444 # should use sudo here
445 if vstatus.cur_selection == '55':
446 vstatus.mk.set_message('OPENSESAME')
447 logging.info('dispensing a door for %s'%vstatus.username)
449 #ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
450 ret = os.system('dispense -u "%s" door'%vstatus.username)
452 ret = os.system('dispense door')
454 logging.info('door opened')
455 vstatus.mk.set_message(center('DOOR OPEN'))
457 logging.warning('user %s tried to dispense a bad door'%vstatus.username)
458 vstatus.mk.set_message(center('BAD DOOR'))
460 elif vstatus.cur_selection == '81':
462 elif vstatus.cur_selection == '99':
463 scroll_options(vstatus.username, vstatus.mk)
464 vstatus.cur_selection = ''
466 elif vstatus.cur_selection[1] == '8':
467 v.display('GOT DRINK?')
468 if ((os.system('dispense -u "%s" coke:%s'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
469 v.display('SEEMS NOT')
471 v.display('GOT DRINK!')
473 # first see if it's a named slot
475 price, shortname, name = get_snack( vstatus.cur_selection )
477 price, shortname, name = get_snack( '--' )
478 dollarprice = "$%.2f" % ( price / 100.0 )
479 v.display(vstatus.cur_selection+' - %s'%dollarprice)
480 # exitcode = os.system('dispense -u "%s" give \>snacksales %d "%s"'%(vstatus.username, price, name)) >> 8
481 # exitcode = os.system('dispense -u "%s" give \>sales\:snack %d "%s"'%(vstatus.username, price, name)) >> 8
482 exitcode = os.system('dispense -u "%s" snack:%s'%(vstatus.username, vstatus.cur_selection)) >> 8
484 # magic dispense syslog service
485 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
486 (worked, code, string) = v.vend(vstatus.cur_selection)
488 v.display('THANK YOU')
490 print "Vend Failed:", code, string
491 v.display('VEND FAIL')
492 elif (exitcode == 5): # RV_BALANCE
493 v.display('NO MONEY?')
494 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
495 v.display('EMPTY SLOT')
496 elif (exitcode == 1): # RV_BADITEM (Dead slot)
497 v.display('EMPTY SLOT')
499 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
500 v.display('UNK ERROR')
504 def price_check(v, vstatus):
505 if vstatus.cur_selection[1] == '8':
506 args = ('dispense', 'iteminfo', 'coke:' + vstatus.cur_selection[0])
507 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
508 dollarprice = re.match("\s*[a-z]+:\d+\s+(\d+\.\d\d)\s+([^\n]+)", info).group(1)
510 # first see if it's a named slot
512 price, shortname, name = get_snack( vstatus.cur_selection )
514 price, shortname, name = get_snack( '--' )
515 dollarprice = "$%.2f" % ( price / 100.0 )
516 v.display(vstatus.cur_selection+' - %s'%dollarprice)
519 def handle_getting_pin_key(state, event, params, v, vstatus):
520 #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
522 if len(vstatus.cur_pin) < PIN_LENGTH:
524 if vstatus.cur_pin == '':
525 vstatus.cur_user = ''
526 reset_idler(v, vstatus)
530 vstatus.mk.set_message('PIN: ')
532 vstatus.cur_pin += chr(key + ord('0'))
533 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
534 if len(vstatus.cur_pin) == PIN_LENGTH:
535 vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
538 vstatus.cur_selection = ''
539 vstatus.change_state(STATE_GET_SELECTION)
540 scroll_options(vstatus.username, vstatus.mk, True)
544 vstatus.mk.set_messages(
545 [(center('BAD PIN'), False, 1.0),
546 (center('SORRY'), False, 0.5)])
547 vstatus.cur_user = ''
550 reset_idler(v, vstatus, 2)
555 def handle_getting_uid_key(state, event, params, v, vstatus):
556 #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
559 # complicated key handling here:
561 if len(vstatus.cur_user) == 0 and key == 9:
562 vstatus.cur_selection = ''
563 vstatus.time_to_autologout = None
564 vstatus.mk.set_message('PRICECHECK')
566 scroll_options('', vstatus.mk)
567 vstatus.change_state(STATE_GET_SELECTION)
570 if len(vstatus.cur_user) <8:
572 vstatus.cur_user = ''
574 reset_idler(v, vstatus)
576 vstatus.cur_user += chr(key + ord('0'))
577 #logging.info('dob: '+vstatus.cur_user)
578 if len(vstatus.cur_user) > 5:
579 vstatus.mk.set_message('>'+vstatus.cur_user)
581 vstatus.mk.set_message('UID: '+vstatus.cur_user)
583 if len(vstatus.cur_user) == 5:
584 uid = int(vstatus.cur_user)
587 logging.info('user '+vstatus.cur_user+' has a bad PIN')
593 Welcome to Picklevision Sytems, Sunnyvale, CA
595 Greetings Professor Falken.
600 Shall we play a game?
603 Please choose from the following menu:
610 6. Toxic and Biochemical Warfare
611 7. Global Thermonuclear War
615 Wouldn't you prefer a nice game of chess?
617 """.replace('\n',' ')
618 vstatus.mk.set_messages([(pfalken, False, 10)])
619 vstatus.cur_user = ''
622 reset_idler(v, vstatus, 10)
626 if not has_good_pin(uid):
627 logging.info('user '+vstatus.cur_user+' has a bad PIN')
628 vstatus.mk.set_messages(
629 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
630 vstatus.cur_user = ''
633 reset_idler(v, vstatus, 3)
637 if acct_is_disabled():
638 logging.info('user '+vstatus.cur_user+' is disabled')
639 vstatus.mk.set_messages(
640 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
641 vstatus.cur_user = ''
644 reset_idler(v, vstatus, 3)
649 vstatus.mk.set_message('PIN: ')
650 logging.info('need pin for user %s'%vstatus.cur_user)
651 vstatus.change_state(STATE_GETTING_PIN)
655 def handle_idle_key(state, event, params, v, vstatus):
656 #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
661 vstatus.cur_user = ''
662 reset_idler(v, vstatus)
665 vstatus.change_state(STATE_GETTING_UID)
666 run_handler(event, key, v, vstatus)
669 def handle_idle_tick(state, event, params, v, vstatus):
671 if vstatus.mk.done():
674 if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
675 vstatus.time_of_next_idler = time() + 30
680 vstatus.mk.update_display()
682 vstatus.change_state(STATE_GRANDFATHER_CLOCK)
683 run_handler(event, params, v, vstatus)
686 def beep_on(when, before=0):
687 start = int(when - before)
691 if now >= start and now <= end:
695 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
696 ### check for interesting times
699 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
700 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
701 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
702 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
704 hourfromnow = localtime(time() + 3600)
706 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
707 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
708 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
710 ## check for X seconds to the hour
711 ## if case, update counter to 2
712 if beep_on(onthehour,15) \
713 or beep_on(halfhour,0) \
714 or beep_on(quarterhour,0) \
715 or beep_on(threequarterhour,0) \
716 or beep_on(fivetothehour,0):
717 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
718 run_handler(event, params, v, vstatus)
720 vstatus.change_state(STATE_IDLE)
722 def handle_grandfather_tick(state, event, params, v, vstatus):
726 ### we live in interesting times
729 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
730 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
731 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
732 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
734 hourfromnow = localtime(time() + 3600)
736 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
737 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
738 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
741 #print "when it fashionable to wear a onion on your hip"
743 if beep_on(onthehour,15):
745 next_hour=((hourfromnow[3] + 11) % 12) + 1
746 if onthehour - time() < next_hour and onthehour - time() > 0:
751 msg.append(("DING!", False, None))
753 msg.append((" DING!", False, None))
754 elif int(onthehour - time()) == 0:
756 msg.append((" BONG!", False, None))
757 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
758 elif beep_on(halfhour,0):
761 msg.append((" HALFHOUR ", False, 50))
762 elif beep_on(quarterhour,0):
765 msg.append((" QTR HOUR ", False, 50))
766 elif beep_on(threequarterhour,0):
769 msg.append((" 3 QTR HR ", False, 50))
770 elif beep_on(fivetothehour,0):
773 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
777 ## check for X seconds to the hour
780 vstatus.mk.set_messages(msg)
783 vstatus.mk.update_display()
784 ## if no longer case, return to idle
786 ## change idler to be clock
787 if go_idle and vstatus.mk.done():
788 vstatus.change_state(STATE_IDLE,1)
790 def handle_door_idle(state, event, params, v, vstatus):
791 def twiddle(clock,v,wise = 2):
793 v.display("-FEED ME-")
794 elif (clock % 4 == 1+wise):
795 v.display("\\FEED ME/")
796 elif (clock % 4 == 2):
797 v.display("-FEED ME-")
798 elif (clock % 4 == 3-wise):
799 v.display("/FEED ME\\")
801 # don't care right now.
804 if ((now % 60 % 2) == 0):
807 twiddle(now, v, wise=0)
810 def handle_door_event(state, event, params, v, vstatus):
811 if params == 0: #door open
812 vstatus.change_state(STATE_DOOR_OPENING)
813 logging.warning("Entering open door mode")
814 v.display("-FEED ME-")
816 vstatus.cur_user = ''
818 elif params == 1: #door closed
819 vstatus.change_state(STATE_DOOR_CLOSING)
820 reset_idler(v, vstatus, 3)
822 logging.warning('Leaving open door mode')
823 v.display("-YUM YUM!-")
825 def handle_mifare_event(state, event, params, v, vstatus):
827 # Translate card_id into uid.
832 vstatus.cur_user = get_uid(card_id)
833 logging.info('Mapped card id to uid %s'%vstatus.cur_user)
834 vstatus.username = get_uname(vstatus.cur_user)
835 if acct_is_disabled(vstatus.username):
836 vstatus.username = '-disabled-'
838 vstatus.username = None
839 if vstatus.username == '-disabled-':
841 vstatus.mk.set_messages(
842 [(center('ACCT DISABLED'), False, 1.0),
843 (center('SORRY'), False, 0.5)])
844 vstatus.cur_user = ''
846 vstatus.username = None
848 reset_idler(v, vstatus, 2)
850 elif vstatus.username:
852 vstatus.cur_selection = ''
853 vstatus.change_state(STATE_GET_SELECTION)
854 scroll_options(vstatus.username, vstatus.mk, True)
858 vstatus.mk.set_messages(
859 [(center('BAD CARD'), False, 1.0),
860 (center('SORRY'), False, 0.5)])
861 vstatus.cur_user = ''
864 reset_idler(v, vstatus, 2)
867 def handle_mifare_add_user_event(state, event, params, v, vstatus):
870 # Translate card_id into uid.
875 if get_uid(card_id) != None:
876 vstatus.mk.set_messages(
877 [(center('ALREADY'), False, 0.5),
878 (center('ENROLLED'), False, 0.5)])
880 # scroll_options(vstatus.username, vstatus.mk)
885 logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
886 set_card_id(vstatus.cur_user, card_id)
887 vstatus.mk.set_messages(
888 [(center('CARD'), False, 0.5),
889 (center('ENROLLED'), False, 0.5)])
891 # scroll_options(vstatus.username, vstatus.mk)
893 def return_to_idle(state,event,params,v,vstatus):
894 reset_idler(v, vstatus)
896 def create_state_table(vstatus):
897 vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
898 vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
899 vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
900 vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
902 vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
903 vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
904 vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
905 vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
907 vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
908 vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
909 vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
910 vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
912 vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
913 vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
914 vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
915 vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
917 vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
918 vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
919 vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
920 vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
922 vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
923 vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
924 vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
925 vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
927 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
928 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
929 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
930 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
931 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
932 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
933 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
935 def get_state_table_handler(vstatus, state, event, counter):
936 return vstatus.state_table[(state,event,counter)]
938 def time_to_next_update(vstatus):
939 idle_update = vstatus.time_of_next_idlestep - time()
940 if not vstatus.mk.done() and vstatus.mk.next_update is not None:
941 mk_update = vstatus.mk.next_update - time()
942 if mk_update < idle_update:
943 idle_update = mk_update
946 def run_forever(rfh, wfh, options, cf):
947 v = VendingMachine(rfh, wfh, USE_MIFARE)
948 vstatus = VendState(v)
949 create_state_table(vstatus)
951 logging.debug('PING is ' + str(v.ping()))
953 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
956 reset_idler(v, vstatus)
958 # This main loop was hideous and the work of the devil.
959 # This has now been fixed (mostly) - mtearle
962 # notes for later surgery
963 # (event, counter, ' ')
967 # ( return state - not currently implemented )
973 except DispenseDatabaseException, e:
974 logging.error('Database error: '+str(e))
976 timeout = time_to_next_update(vstatus)
977 e = v.next_event(timeout)
980 run_handler(event, params, v, vstatus)
982 # logging.debug('Got event: ' + repr(e))
985 def run_handler(event, params, v, vstatus):
986 handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
988 handler(vstatus.state, event, params, v, vstatus)
990 def connect_to_vend(options, cf):
993 logging.info('Connecting to vending machine using LAT')
994 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
995 rfh, wfh = latclient.get_fh()
996 elif options.use_serial:
997 # Open vending machine via serial.
998 logging.info('Connecting to vending machine using serial')
999 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
1000 rfh,wfh = serialclient.get_fh()
1002 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
1003 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
1005 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
1006 sock.connect((options.host, options.port))
1007 rfh = sock.makefile('r')
1008 wfh = sock.makefile('w')
1015 from optparse import OptionParser
1017 op = OptionParser(usage="%prog [OPTION]...")
1018 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')
1019 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
1020 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
1021 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
1022 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
1023 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
1024 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
1025 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
1026 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
1027 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
1028 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
1029 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
1030 options, args = op.parse_args()
1033 op.error('extra command line arguments: ' + ' '.join(args))
1038 'DBServer': ('Database', 'Server'),
1039 'DBName': ('Database', 'Name'),
1040 'DBUser': ('VendingMachine', 'DBUser'),
1041 'DBPassword': ('VendingMachine', 'DBPassword'),
1043 'ServiceName': ('VendingMachine', 'ServiceName'),
1044 'ServicePassword': ('VendingMachine', 'Password'),
1046 'ServerName': ('DecServer', 'Name'),
1047 'ConnectPassword': ('DecServer', 'ConnectPassword'),
1048 'PrivPassword': ('DecServer', 'PrivPassword'),
1051 class VendConfigFile:
1052 def __init__(self, config_file, options):
1054 cp = ConfigParser.ConfigParser()
1055 cp.read(config_file)
1057 for option in options:
1058 section, name = options[option]
1059 value = cp.get(section, name)
1060 self.__dict__[option] = value
1062 except ConfigParser.Error, e:
1063 raise SystemExit("Error reading config file "+config_file+": " + str(e))
1065 def create_pid_file(name):
1067 pid_file = file(name, 'w')
1068 pid_file.write('%d\n'%os.getpid())
1071 logging.warning('unable to write to pid file '+name+': '+str(e))
1074 def do_nothing(signum, stack):
1075 signal.signal(signum, do_nothing)
1076 def stop_server(signum, stack): raise KeyboardInterrupt
1077 signal.signal(signal.SIGHUP, do_nothing)
1078 signal.signal(signal.SIGTERM, stop_server)
1079 signal.signal(signal.SIGINT, stop_server)
1081 options = parse_args()
1082 config_opts = VendConfigFile(options.config_file, config_options)
1083 if options.daemon: become_daemon()
1084 set_up_logging(options)
1085 if options.pid_file != '': create_pid_file(options.pid_file)
1087 return options, config_opts
1089 def clean_up_nicely(options, config_opts):
1090 if options.pid_file != '':
1092 os.unlink(options.pid_file)
1093 logging.debug('Removed pid file '+options.pid_file)
1094 except OSError: pass # if we can't delete it, meh
1096 def set_up_logging(options):
1097 logger = logging.getLogger()
1099 if not options.daemon:
1100 stderr_logger = logging.StreamHandler(sys.stderr)
1101 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1102 logger.addHandler(stderr_logger)
1104 if options.log_file != '':
1106 file_logger = logging.FileHandler(options.log_file)
1107 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1108 logger.addHandler(file_logger)
1110 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1112 if options.syslog != None:
1113 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1114 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1115 logger.addHandler(sys_logger)
1118 logger.setLevel(logging.WARNING)
1119 elif options.verbose:
1120 logger.setLevel(logging.DEBUG)
1122 logger.setLevel(logging.INFO)
1124 def become_daemon():
1125 dev_null = file('/dev/null')
1126 fd = dev_null.fileno()
1135 raise SystemExit('failed to fork: '+str(e))
1137 def do_vend_server(options, config_opts):
1140 rfh, wfh = connect_to_vend(options, config_opts)
1141 except (SerialClientException, socket.error), e:
1142 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1144 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1145 logging.info("Trying again in 5 seconds.")
1149 # run_forever(rfh, wfh, options, config_opts)
1152 run_forever(rfh, wfh, options, config_opts)
1153 except VendingException:
1154 logging.error("Connection died, trying again...")
1155 logging.info("Trying again in 5 seconds.")
1158 if __name__ == '__main__':
1159 options, config_opts = set_stuff_up()
1162 logging.warning('Starting Vend Server')
1163 do_vend_server(options, config_opts)
1164 logging.error('Vend Server finished unexpectedly, restarting')
1165 except KeyboardInterrupt:
1166 logging.info("Killed by signal, cleaning up")
1167 clean_up_nicely(options, config_opts)
1168 logging.warning("Vend Server stopped")
1173 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1174 tb = format_tb(exc_traceback, 20)
1177 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1178 logging.critical("Message: " + str(exc_value))
1179 logging.critical("Traceback:")
1181 for line in event.split('\n'):
1182 logging.critical(' '+line)
1183 logging.critical("This message should be considered a bug in the Vend Server.")
1184 logging.critical("Please report this to someone who can fix it.")
1186 logging.warning("Trying again anyway (might not help, but hey...)")