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, 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))
143 info = pwd.getpwuid(uid)
145 logging.info('getting pin for uid %d: user not in password file'%uid)
147 if info.pw_dir == None: return False
148 pinfile = os.path.join(info.pw_dir, '.pin')
152 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
155 logging.info('getting pin for uid %d: .pin has wrong permissions. Fixing.'%uid)
156 os.chmod(pinfile, 0600)
160 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
162 pinstr = f.readline()
164 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
165 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
169 def has_good_pin(uid):
170 return get_pin(uid) != None
172 def verify_user_pin(uid, pin, skip_pin_check=False):
173 if skip_pin_check or get_pin(uid) == pin:
174 info = pwd.getpwuid(uid)
176 logging.info('accepted mifare for uid %d (%s)'%(uid,info.pw_name))
178 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
181 logging.info('refused pin for uid %d'%(uid))
187 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
188 choice = int(random()*len(messages))
189 msg = messages[choice]
190 left = range(len(msg))
191 for i in range(len(msg)):
192 if msg[i] == ' ': left.remove(i)
196 for i in range(0, len(msg)):
202 s += chr(int(random()*26)+ord('A'))
211 return ' '*((LEN-len(str))/2)+str
222 StringIdler(v, text="Kill 'em all", repeat=False),
223 GrayIdler(v,one="*",zero="-"),
224 StringIdler(v, text=CREDITS),
225 GrayIdler(v,one="/",zero="\\"),
227 GrayIdler(v,one="X",zero="O"),
228 FileIdler(v, '/usr/share/common-licenses/GPL-2',affinity=2),
229 GrayIdler(v,one="*",zero="-",reorder=1),
230 StringIdler(v, text=str(math.pi) + " "),
232 GrayIdler(v,one="/",zero="\\",reorder=1),
233 StringIdler(v, text=str(math.e) + " "),
234 GrayIdler(v,one="X",zero="O",reorder=1),
235 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),
236 PipeIdler(v, "/usr/bin/getent", "passwd"),
245 def reset_idler(v, vstatus, t = None):
247 idler = GreetingIdler(v, t)
248 vstatus.time_of_next_idlestep = time()+idler.next()
249 vstatus.time_of_next_idler = None
250 vstatus.time_to_autologout = None
251 vstatus.change_state(STATE_IDLE, 1)
256 average_affinity = 10 # guessing here...
258 if idler and idler.__class__ != GreetingIdler:
259 iiindex = idlers.index(idler)
263 move = int(random()*len(idlers)*average_affinity) + 1
268 idler = idlers[iiindex]
269 move -= idler.affinity()
273 def idle_step(vstatus):
277 vstatus.time_of_next_idler = time() + 30
278 nextidle = idler.next()
280 nextidle = IDLE_SPEED
281 vstatus.time_of_next_idlestep = time()+nextidle
284 def __init__(self,v):
285 self.state_table = {}
286 self.state = STATE_IDLE
289 self.mk = MessageKeeper(v)
293 self.cur_selection = ''
294 self.time_to_autologout = None
296 self.last_timeout_refresh = None
298 def change_state(self,newstate,newcounter=None):
299 if self.state != newstate:
300 #print "Changing state from: ",
304 self.state = newstate
306 if newcounter is not None and self.counter != newcounter:
307 #print "Changing counter from: ",
311 self.counter = newcounter
315 def handle_tick_event(event, params, v, vstatus):
316 # don't care right now.
319 def handle_switch_event(event, params, v, vstatus):
320 # don't care right now.
324 def do_nothing(state, event, params, v, vstatus):
325 print "doing nothing (s,e,p)", state, " ", event, " ", params
328 def handle_getting_uid_idle(state, event, params, v, vstatus):
329 # don't care right now.
332 def handle_getting_pin_idle(state, event, params, v, vstatus):
333 # don't care right now.
336 def handle_get_selection_idle(state, event, params, v, vstatus):
337 # don't care right now.
339 ### State logging out ..
340 if vstatus.time_to_autologout != None:
341 time_left = vstatus.time_to_autologout - time()
342 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
343 vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
344 vstatus.last_timeout_refresh = int(time_left)
345 vstatus.cur_selection = ''
347 if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
348 vstatus.time_to_autologout = None
349 vstatus.cur_user = ''
351 vstatus.cur_selection = ''
353 reset_idler(v, vstatus)
355 ### State fully logged out ... reset variables
356 if vstatus.time_to_autologout and not vstatus.mk.done():
357 vstatus.time_to_autologout = None
358 if vstatus.cur_user == '' and vstatus.time_to_autologout:
359 vstatus.time_to_autologout = None
362 if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
364 vstatus.time_to_autologout = time() + 15
365 vstatus.last_timeout_refresh = None
367 ## FIXME - this may need to be elsewhere.....
369 vstatus.mk.update_display()
373 def handle_get_selection_key(state, event, params, v, vstatus):
375 if len(vstatus.cur_selection) == 0:
378 vstatus.cur_user = ''
379 vstatus.cur_selection = ''
381 vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
382 reset_idler(v, vstatus, 2)
384 vstatus.cur_selection += chr(key + ord('0'))
385 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
386 vstatus.time_to_autologout = None
387 elif len(vstatus.cur_selection) == 1:
389 vstatus.cur_selection = ''
390 vstatus.time_to_autologout = None
391 scroll_options(vstatus.username, vstatus.mk)
394 vstatus.cur_selection += chr(key + ord('0'))
396 make_selection(v,vstatus)
397 vstatus.cur_selection = ''
398 vstatus.time_to_autologout = time() + 8
399 vstatus.last_timeout_refresh = None
402 price_check(v,vstatus)
403 vstatus.cur_selection = ''
404 vstatus.time_to_autologout = None
405 vstatus.last_timeout_refresh = None
407 def make_selection(v, vstatus):
408 # should use sudo here
409 if vstatus.cur_selection == '55':
410 vstatus.mk.set_message('OPENSESAME')
411 logging.info('dispensing a door for %s'%vstatus.username)
413 #ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
414 ret = os.system('dispense -u "%s" door'%vstatus.username)
416 ret = os.system('dispense door')
418 logging.info('door opened')
419 vstatus.mk.set_message(center('DOOR OPEN'))
421 logging.warning('user %s tried to dispense a bad door'%vstatus.username)
422 vstatus.mk.set_message(center('BAD DOOR'))
424 elif vstatus.cur_selection == '81':
426 elif vstatus.cur_selection == '99':
427 scroll_options(vstatus.username, vstatus.mk)
428 vstatus.cur_selection = ''
430 elif vstatus.cur_selection[1] == '8':
431 v.display('GOT DRINK?')
432 if ((os.system('dispense -u "%s" coke:%s'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
433 v.display('SEEMS NOT')
435 v.display('GOT DRINK!')
437 # first see if it's a named slot
439 price, shortname, name = get_snack( vstatus.cur_selection )
441 price, shortname, name = get_snack( '--' )
442 dollarprice = "$%.2f" % ( price / 100.0 )
443 v.display(vstatus.cur_selection+' - %s'%dollarprice)
444 exitcode = os.system('dispense -u "%s" give \>snacksales %d "%s"'%(vstatus.username, price, name)) >> 8
445 # exitcode = os.system('dispense -u "%s" give \>sales\:snack %d "%s"'%(vstatus.username, price, name)) >> 8
446 # exitcode = os.system('dispense -u "%s" snack:%s'%(vstatus.username, vstatus.cur_selection)) >> 8
448 # magic dispense syslog service
449 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
450 v.vend(vstatus.cur_selection)
451 v.display('THANK YOU')
452 elif (exitcode == 5): # RV_BALANCE
453 v.display('NO MONEY?')
454 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
455 v.display('EMPTY SLOT')
456 elif (exitcode == 1): # RV_BADITEM (Dead slot)
457 v.display('EMPTY SLOT')
459 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
460 v.display('UNK ERROR')
464 def price_check(v, vstatus):
465 if vstatus.cur_selection[1] == '8':
466 v.display(center('SEE COKE'))
468 # first see if it's a named slot
470 price, shortname, name = get_snack( vstatus.cur_selection )
472 price, shortname, name = get_snack( '--' )
473 dollarprice = "$%.2f" % ( price / 100.0 )
474 v.display(vstatus.cur_selection+' - %s'%dollarprice)
477 def handle_getting_pin_key(state, event, params, v, vstatus):
478 #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
480 if len(vstatus.cur_pin) < PIN_LENGTH:
482 if vstatus.cur_pin == '':
483 vstatus.cur_user = ''
484 reset_idler(v, vstatus)
488 vstatus.mk.set_message('PIN: ')
490 vstatus.cur_pin += chr(key + ord('0'))
491 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
492 if len(vstatus.cur_pin) == PIN_LENGTH:
493 vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
496 vstatus.cur_selection = ''
497 vstatus.change_state(STATE_GET_SELECTION)
498 scroll_options(vstatus.username, vstatus.mk, True)
502 vstatus.mk.set_messages(
503 [(center('BAD PIN'), False, 1.0),
504 (center('SORRY'), False, 0.5)])
505 vstatus.cur_user = ''
508 reset_idler(v, vstatus, 2)
513 def handle_getting_uid_key(state, event, params, v, vstatus):
514 #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
517 # complicated key handling here:
519 if len(vstatus.cur_user) == 0 and key == 9:
520 vstatus.cur_selection = ''
521 vstatus.time_to_autologout = None
522 vstatus.mk.set_message('PRICECHECK')
524 scroll_options('', vstatus.mk)
525 vstatus.change_state(STATE_GET_SELECTION)
528 if len(vstatus.cur_user) <8:
530 vstatus.cur_user = ''
532 reset_idler(v, vstatus)
534 vstatus.cur_user += chr(key + ord('0'))
535 #logging.info('dob: '+vstatus.cur_user)
536 if len(vstatus.cur_user) > 5:
537 vstatus.mk.set_message('>'+vstatus.cur_user)
539 vstatus.mk.set_message('UID: '+vstatus.cur_user)
541 if len(vstatus.cur_user) == 5:
542 uid = int(vstatus.cur_user)
545 logging.info('user '+vstatus.cur_user+' has a bad PIN')
551 Welcome to Picklevision Sytems, Sunnyvale, CA
553 Greetings Professor Falken.
558 Shall we play a game?
561 Please choose from the following menu:
568 6. Toxic and Biochemical Warfare
569 7. Global Thermonuclear War
573 Wouldn't you prefer a nice game of chess?
575 """.replace('\n',' ')
576 vstatus.mk.set_messages([(pfalken, False, 10)])
577 vstatus.cur_user = ''
580 reset_idler(v, vstatus, 10)
584 if not has_good_pin(uid):
585 logging.info('user '+vstatus.cur_user+' has a bad PIN')
586 vstatus.mk.set_messages(
587 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
588 vstatus.cur_user = ''
591 reset_idler(v, vstatus, 3)
597 vstatus.mk.set_message('PIN: ')
598 logging.info('need pin for user %s'%vstatus.cur_user)
599 vstatus.change_state(STATE_GETTING_PIN)
603 def handle_idle_key(state, event, params, v, vstatus):
604 #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
609 vstatus.cur_user = ''
610 reset_idler(v, vstatus)
613 vstatus.change_state(STATE_GETTING_UID)
614 run_handler(event, key, v, vstatus)
617 def handle_idle_tick(state, event, params, v, vstatus):
619 if vstatus.mk.done():
622 if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
623 vstatus.time_of_next_idler = time() + 30
628 vstatus.mk.update_display()
630 vstatus.change_state(STATE_GRANDFATHER_CLOCK)
631 run_handler(event, params, v, vstatus)
634 def beep_on(when, before=0):
635 start = int(when - before)
639 if now >= start and now <= end:
643 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
644 ### check for interesting times
647 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
648 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
649 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
650 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
652 hourfromnow = localtime(time() + 3600)
654 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
655 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
656 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
658 ## check for X seconds to the hour
659 ## if case, update counter to 2
660 if beep_on(onthehour,15) \
661 or beep_on(halfhour,0) \
662 or beep_on(quarterhour,0) \
663 or beep_on(threequarterhour,0) \
664 or beep_on(fivetothehour,0):
665 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
666 run_handler(event, params, v, vstatus)
668 vstatus.change_state(STATE_IDLE)
670 def handle_grandfather_tick(state, event, params, v, vstatus):
674 ### we live in interesting times
677 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
678 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
679 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
680 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
682 hourfromnow = localtime(time() + 3600)
684 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
685 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
686 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
689 #print "when it fashionable to wear a onion on your hip"
691 if beep_on(onthehour,15):
693 next_hour=((hourfromnow[3] + 11) % 12) + 1
694 if onthehour - time() < next_hour and onthehour - time() > 0:
699 msg.append(("DING!", False, None))
701 msg.append((" DING!", False, None))
702 elif int(onthehour - time()) == 0:
704 msg.append((" BONG!", False, None))
705 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
706 elif beep_on(halfhour,0):
709 msg.append((" HALFHOUR ", False, 50))
710 elif beep_on(quarterhour,0):
713 msg.append((" QTR HOUR ", False, 50))
714 elif beep_on(threequarterhour,0):
717 msg.append((" 3 QTR HR ", False, 50))
718 elif beep_on(fivetothehour,0):
721 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
725 ## check for X seconds to the hour
728 vstatus.mk.set_messages(msg)
731 vstatus.mk.update_display()
732 ## if no longer case, return to idle
734 ## change idler to be clock
735 if go_idle and vstatus.mk.done():
736 vstatus.change_state(STATE_IDLE,1)
738 def handle_door_idle(state, event, params, v, vstatus):
739 def twiddle(clock,v,wise = 2):
741 v.display("-FEED ME-")
742 elif (clock % 4 == 1+wise):
743 v.display("\\FEED ME/")
744 elif (clock % 4 == 2):
745 v.display("-FEED ME-")
746 elif (clock % 4 == 3-wise):
747 v.display("/FEED ME\\")
749 # don't care right now.
752 if ((now % 60 % 2) == 0):
755 twiddle(now, v, wise=0)
758 def handle_door_event(state, event, params, v, vstatus):
759 if params == 0: #door open
760 vstatus.change_state(STATE_DOOR_OPENING)
761 logging.warning("Entering open door mode")
762 v.display("-FEED ME-")
764 vstatus.cur_user = ''
766 elif params == 1: #door closed
767 vstatus.change_state(STATE_DOOR_CLOSING)
768 reset_idler(v, vstatus, 3)
770 logging.warning('Leaving open door mode')
771 v.display("-YUM YUM!-")
773 def handle_mifare_event(state, event, params, v, vstatus):
775 # Translate card_id into uid.
780 vstatus.cur_user = get_uid(card_id)
781 logging.info('Mapped card id to uid %s'%vstatus.cur_user)
782 vstatus.username = verify_user_pin(int(vstatus.cur_user), None, True)
784 vstatus.username = None
787 vstatus.cur_selection = ''
788 vstatus.change_state(STATE_GET_SELECTION)
789 scroll_options(vstatus.username, vstatus.mk, True)
793 vstatus.mk.set_messages(
794 [(center('BAD CARD'), False, 1.0),
795 (center('SORRY'), False, 0.5)])
796 vstatus.cur_user = ''
799 reset_idler(v, vstatus, 2)
802 def handle_mifare_add_user_event(state, event, params, v, vstatus):
805 # Translate card_id into uid.
810 if get_uid(card_id) != None:
811 vstatus.mk.set_messages(
812 [(center('ALREADY'), False, 0.5),
813 (center('ENROLLED'), False, 0.5)])
815 # scroll_options(vstatus.username, vstatus.mk)
820 logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
821 set_card_id(vstatus.cur_user, card_id)
822 vstatus.mk.set_messages(
823 [(center('CARD'), False, 0.5),
824 (center('ENROLLED'), False, 0.5)])
826 # scroll_options(vstatus.username, vstatus.mk)
828 def return_to_idle(state,event,params,v,vstatus):
829 reset_idler(v, vstatus)
831 def create_state_table(vstatus):
832 vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
833 vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
834 vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
835 vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
837 vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
838 vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
839 vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
840 vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
842 vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
843 vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
844 vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
845 vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
847 vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
848 vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
849 vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
850 vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
852 vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
853 vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
854 vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
855 vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
857 vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
858 vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
859 vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
860 vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
862 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
863 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
864 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
865 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
866 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
867 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
868 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
870 def get_state_table_handler(vstatus, state, event, counter):
871 return vstatus.state_table[(state,event,counter)]
873 def time_to_next_update(vstatus):
874 idle_update = vstatus.time_of_next_idlestep - time()
875 if not vstatus.mk.done() and vstatus.mk.next_update is not None:
876 mk_update = vstatus.mk.next_update - time()
877 if mk_update < idle_update:
878 idle_update = mk_update
881 def run_forever(rfh, wfh, options, cf):
882 v = VendingMachine(rfh, wfh, USE_MIFARE)
883 vstatus = VendState(v)
884 create_state_table(vstatus)
886 logging.debug('PING is ' + str(v.ping()))
888 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
891 reset_idler(v, vstatus)
893 # This main loop was hideous and the work of the devil.
894 # This has now been fixed (mostly) - mtearle
897 # notes for later surgery
898 # (event, counter, ' ')
902 # ( return state - not currently implemented )
908 except DispenseDatabaseException, e:
909 logging.error('Database error: '+str(e))
911 timeout = time_to_next_update(vstatus)
912 e = v.next_event(timeout)
915 run_handler(event, params, v, vstatus)
917 # logging.debug('Got event: ' + repr(e))
920 def run_handler(event, params, v, vstatus):
921 handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
923 handler(vstatus.state, event, params, v, vstatus)
925 def connect_to_vend(options, cf):
928 logging.info('Connecting to vending machine using LAT')
929 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
930 rfh, wfh = latclient.get_fh()
931 elif options.use_serial:
932 # Open vending machine via serial.
933 logging.info('Connecting to vending machine using serial')
934 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
935 rfh,wfh = serialclient.get_fh()
937 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
938 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
940 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
941 sock.connect((options.host, options.port))
942 rfh = sock.makefile('r')
943 wfh = sock.makefile('w')
950 from optparse import OptionParser
952 op = OptionParser(usage="%prog [OPTION]...")
953 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')
954 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
955 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
956 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
957 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
958 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
959 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
960 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
961 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
962 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
963 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
964 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
965 options, args = op.parse_args()
968 op.error('extra command line arguments: ' + ' '.join(args))
973 'DBServer': ('Database', 'Server'),
974 'DBName': ('Database', 'Name'),
975 'DBUser': ('VendingMachine', 'DBUser'),
976 'DBPassword': ('VendingMachine', 'DBPassword'),
978 'ServiceName': ('VendingMachine', 'ServiceName'),
979 'ServicePassword': ('VendingMachine', 'Password'),
981 'ServerName': ('DecServer', 'Name'),
982 'ConnectPassword': ('DecServer', 'ConnectPassword'),
983 'PrivPassword': ('DecServer', 'PrivPassword'),
986 class VendConfigFile:
987 def __init__(self, config_file, options):
989 cp = ConfigParser.ConfigParser()
992 for option in options:
993 section, name = options[option]
994 value = cp.get(section, name)
995 self.__dict__[option] = value
997 except ConfigParser.Error, e:
998 raise SystemExit("Error reading config file "+config_file+": " + str(e))
1000 def create_pid_file(name):
1002 pid_file = file(name, 'w')
1003 pid_file.write('%d\n'%os.getpid())
1006 logging.warning('unable to write to pid file '+name+': '+str(e))
1009 def do_nothing(signum, stack):
1010 signal.signal(signum, do_nothing)
1011 def stop_server(signum, stack): raise KeyboardInterrupt
1012 signal.signal(signal.SIGHUP, do_nothing)
1013 signal.signal(signal.SIGTERM, stop_server)
1014 signal.signal(signal.SIGINT, stop_server)
1016 options = parse_args()
1017 config_opts = VendConfigFile(options.config_file, config_options)
1018 if options.daemon: become_daemon()
1019 set_up_logging(options)
1020 if options.pid_file != '': create_pid_file(options.pid_file)
1022 return options, config_opts
1024 def clean_up_nicely(options, config_opts):
1025 if options.pid_file != '':
1027 os.unlink(options.pid_file)
1028 logging.debug('Removed pid file '+options.pid_file)
1029 except OSError: pass # if we can't delete it, meh
1031 def set_up_logging(options):
1032 logger = logging.getLogger()
1034 if not options.daemon:
1035 stderr_logger = logging.StreamHandler(sys.stderr)
1036 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1037 logger.addHandler(stderr_logger)
1039 if options.log_file != '':
1041 file_logger = logging.FileHandler(options.log_file)
1042 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1043 logger.addHandler(file_logger)
1045 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1047 if options.syslog != None:
1048 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1049 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1050 logger.addHandler(sys_logger)
1053 logger.setLevel(logging.WARNING)
1054 elif options.verbose:
1055 logger.setLevel(logging.DEBUG)
1057 logger.setLevel(logging.INFO)
1059 def become_daemon():
1060 dev_null = file('/dev/null')
1061 fd = dev_null.fileno()
1070 raise SystemExit('failed to fork: '+str(e))
1072 def do_vend_server(options, config_opts):
1075 rfh, wfh = connect_to_vend(options, config_opts)
1076 except (SerialClientException, socket.error), e:
1077 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1079 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1080 logging.info("Trying again in 5 seconds.")
1084 # run_forever(rfh, wfh, options, config_opts)
1087 run_forever(rfh, wfh, options, config_opts)
1088 except VendingException:
1089 logging.error("Connection died, trying again...")
1090 logging.info("Trying again in 5 seconds.")
1093 if __name__ == '__main__':
1094 options, config_opts = set_stuff_up()
1097 logging.warning('Starting Vend Server')
1098 do_vend_server(options, config_opts)
1099 logging.error('Vend Server finished unexpectedly, restarting')
1100 except KeyboardInterrupt:
1101 logging.info("Killed by signal, cleaning up")
1102 clean_up_nicely(options, config_opts)
1103 logging.warning("Vend Server stopped")
1108 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1109 tb = format_tb(exc_traceback, 20)
1112 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1113 logging.critical("Message: " + str(exc_value))
1114 logging.critical("Traceback:")
1116 for line in event.split('\n'):
1117 logging.critical(' '+line)
1118 logging.critical("This message should be considered a bug in the Vend Server.")
1119 logging.critical("Please report this to someone who can fix it.")
1121 logging.warning("Trying again anyway (might not help, but hey...)")