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 args = ('dispense', 'iteminfo', 'coke:' + vstatus.cur_selection[0])
467 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
468 dollarprice = re.match("\s*[a-z]+:\d+\s+(\d+\.\d\d)\s+([^\n]+)", info).group(1)
470 # first see if it's a named slot
472 price, shortname, name = get_snack( vstatus.cur_selection )
474 price, shortname, name = get_snack( '--' )
475 dollarprice = "$%.2f" % ( price / 100.0 )
476 v.display(vstatus.cur_selection+' - %s'%dollarprice)
479 def handle_getting_pin_key(state, event, params, v, vstatus):
480 #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
482 if len(vstatus.cur_pin) < PIN_LENGTH:
484 if vstatus.cur_pin == '':
485 vstatus.cur_user = ''
486 reset_idler(v, vstatus)
490 vstatus.mk.set_message('PIN: ')
492 vstatus.cur_pin += chr(key + ord('0'))
493 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
494 if len(vstatus.cur_pin) == PIN_LENGTH:
495 vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
498 vstatus.cur_selection = ''
499 vstatus.change_state(STATE_GET_SELECTION)
500 scroll_options(vstatus.username, vstatus.mk, True)
504 vstatus.mk.set_messages(
505 [(center('BAD PIN'), False, 1.0),
506 (center('SORRY'), False, 0.5)])
507 vstatus.cur_user = ''
510 reset_idler(v, vstatus, 2)
515 def handle_getting_uid_key(state, event, params, v, vstatus):
516 #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
519 # complicated key handling here:
521 if len(vstatus.cur_user) == 0 and key == 9:
522 vstatus.cur_selection = ''
523 vstatus.time_to_autologout = None
524 vstatus.mk.set_message('PRICECHECK')
526 scroll_options('', vstatus.mk)
527 vstatus.change_state(STATE_GET_SELECTION)
530 if len(vstatus.cur_user) <8:
532 vstatus.cur_user = ''
534 reset_idler(v, vstatus)
536 vstatus.cur_user += chr(key + ord('0'))
537 #logging.info('dob: '+vstatus.cur_user)
538 if len(vstatus.cur_user) > 5:
539 vstatus.mk.set_message('>'+vstatus.cur_user)
541 vstatus.mk.set_message('UID: '+vstatus.cur_user)
543 if len(vstatus.cur_user) == 5:
544 uid = int(vstatus.cur_user)
547 logging.info('user '+vstatus.cur_user+' has a bad PIN')
553 Welcome to Picklevision Sytems, Sunnyvale, CA
555 Greetings Professor Falken.
560 Shall we play a game?
563 Please choose from the following menu:
570 6. Toxic and Biochemical Warfare
571 7. Global Thermonuclear War
575 Wouldn't you prefer a nice game of chess?
577 """.replace('\n',' ')
578 vstatus.mk.set_messages([(pfalken, False, 10)])
579 vstatus.cur_user = ''
582 reset_idler(v, vstatus, 10)
586 if not has_good_pin(uid):
587 logging.info('user '+vstatus.cur_user+' has a bad PIN')
588 vstatus.mk.set_messages(
589 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
590 vstatus.cur_user = ''
593 reset_idler(v, vstatus, 3)
599 vstatus.mk.set_message('PIN: ')
600 logging.info('need pin for user %s'%vstatus.cur_user)
601 vstatus.change_state(STATE_GETTING_PIN)
605 def handle_idle_key(state, event, params, v, vstatus):
606 #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
611 vstatus.cur_user = ''
612 reset_idler(v, vstatus)
615 vstatus.change_state(STATE_GETTING_UID)
616 run_handler(event, key, v, vstatus)
619 def handle_idle_tick(state, event, params, v, vstatus):
621 if vstatus.mk.done():
624 if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
625 vstatus.time_of_next_idler = time() + 30
630 vstatus.mk.update_display()
632 vstatus.change_state(STATE_GRANDFATHER_CLOCK)
633 run_handler(event, params, v, vstatus)
636 def beep_on(when, before=0):
637 start = int(when - before)
641 if now >= start and now <= end:
645 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
646 ### check for interesting times
649 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
650 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
651 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
652 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
654 hourfromnow = localtime(time() + 3600)
656 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
657 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
658 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
660 ## check for X seconds to the hour
661 ## if case, update counter to 2
662 if beep_on(onthehour,15) \
663 or beep_on(halfhour,0) \
664 or beep_on(quarterhour,0) \
665 or beep_on(threequarterhour,0) \
666 or beep_on(fivetothehour,0):
667 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
668 run_handler(event, params, v, vstatus)
670 vstatus.change_state(STATE_IDLE)
672 def handle_grandfather_tick(state, event, params, v, vstatus):
676 ### we live in interesting times
679 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
680 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
681 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
682 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
684 hourfromnow = localtime(time() + 3600)
686 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
687 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
688 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
691 #print "when it fashionable to wear a onion on your hip"
693 if beep_on(onthehour,15):
695 next_hour=((hourfromnow[3] + 11) % 12) + 1
696 if onthehour - time() < next_hour and onthehour - time() > 0:
701 msg.append(("DING!", False, None))
703 msg.append((" DING!", False, None))
704 elif int(onthehour - time()) == 0:
706 msg.append((" BONG!", False, None))
707 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
708 elif beep_on(halfhour,0):
711 msg.append((" HALFHOUR ", False, 50))
712 elif beep_on(quarterhour,0):
715 msg.append((" QTR HOUR ", False, 50))
716 elif beep_on(threequarterhour,0):
719 msg.append((" 3 QTR HR ", False, 50))
720 elif beep_on(fivetothehour,0):
723 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
727 ## check for X seconds to the hour
730 vstatus.mk.set_messages(msg)
733 vstatus.mk.update_display()
734 ## if no longer case, return to idle
736 ## change idler to be clock
737 if go_idle and vstatus.mk.done():
738 vstatus.change_state(STATE_IDLE,1)
740 def handle_door_idle(state, event, params, v, vstatus):
741 def twiddle(clock,v,wise = 2):
743 v.display("-FEED ME-")
744 elif (clock % 4 == 1+wise):
745 v.display("\\FEED ME/")
746 elif (clock % 4 == 2):
747 v.display("-FEED ME-")
748 elif (clock % 4 == 3-wise):
749 v.display("/FEED ME\\")
751 # don't care right now.
754 if ((now % 60 % 2) == 0):
757 twiddle(now, v, wise=0)
760 def handle_door_event(state, event, params, v, vstatus):
761 if params == 0: #door open
762 vstatus.change_state(STATE_DOOR_OPENING)
763 logging.warning("Entering open door mode")
764 v.display("-FEED ME-")
766 vstatus.cur_user = ''
768 elif params == 1: #door closed
769 vstatus.change_state(STATE_DOOR_CLOSING)
770 reset_idler(v, vstatus, 3)
772 logging.warning('Leaving open door mode')
773 v.display("-YUM YUM!-")
775 def handle_mifare_event(state, event, params, v, vstatus):
777 # Translate card_id into uid.
782 vstatus.cur_user = get_uid(card_id)
783 logging.info('Mapped card id to uid %s'%vstatus.cur_user)
784 vstatus.username = verify_user_pin(int(vstatus.cur_user), None, True)
786 vstatus.username = None
789 vstatus.cur_selection = ''
790 vstatus.change_state(STATE_GET_SELECTION)
791 scroll_options(vstatus.username, vstatus.mk, True)
795 vstatus.mk.set_messages(
796 [(center('BAD CARD'), False, 1.0),
797 (center('SORRY'), False, 0.5)])
798 vstatus.cur_user = ''
801 reset_idler(v, vstatus, 2)
804 def handle_mifare_add_user_event(state, event, params, v, vstatus):
807 # Translate card_id into uid.
812 if get_uid(card_id) != None:
813 vstatus.mk.set_messages(
814 [(center('ALREADY'), False, 0.5),
815 (center('ENROLLED'), False, 0.5)])
817 # scroll_options(vstatus.username, vstatus.mk)
822 logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
823 set_card_id(vstatus.cur_user, card_id)
824 vstatus.mk.set_messages(
825 [(center('CARD'), False, 0.5),
826 (center('ENROLLED'), False, 0.5)])
828 # scroll_options(vstatus.username, vstatus.mk)
830 def return_to_idle(state,event,params,v,vstatus):
831 reset_idler(v, vstatus)
833 def create_state_table(vstatus):
834 vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
835 vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
836 vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
837 vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
839 vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
840 vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
841 vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
842 vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
844 vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
845 vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
846 vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
847 vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
849 vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
850 vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
851 vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
852 vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
854 vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
855 vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
856 vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
857 vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
859 vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
860 vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
861 vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
862 vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
864 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
865 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
866 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
867 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
868 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
869 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
870 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
872 def get_state_table_handler(vstatus, state, event, counter):
873 return vstatus.state_table[(state,event,counter)]
875 def time_to_next_update(vstatus):
876 idle_update = vstatus.time_of_next_idlestep - time()
877 if not vstatus.mk.done() and vstatus.mk.next_update is not None:
878 mk_update = vstatus.mk.next_update - time()
879 if mk_update < idle_update:
880 idle_update = mk_update
883 def run_forever(rfh, wfh, options, cf):
884 v = VendingMachine(rfh, wfh, USE_MIFARE)
885 vstatus = VendState(v)
886 create_state_table(vstatus)
888 logging.debug('PING is ' + str(v.ping()))
890 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
893 reset_idler(v, vstatus)
895 # This main loop was hideous and the work of the devil.
896 # This has now been fixed (mostly) - mtearle
899 # notes for later surgery
900 # (event, counter, ' ')
904 # ( return state - not currently implemented )
910 except DispenseDatabaseException, e:
911 logging.error('Database error: '+str(e))
913 timeout = time_to_next_update(vstatus)
914 e = v.next_event(timeout)
917 run_handler(event, params, v, vstatus)
919 # logging.debug('Got event: ' + repr(e))
922 def run_handler(event, params, v, vstatus):
923 handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
925 handler(vstatus.state, event, params, v, vstatus)
927 def connect_to_vend(options, cf):
930 logging.info('Connecting to vending machine using LAT')
931 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
932 rfh, wfh = latclient.get_fh()
933 elif options.use_serial:
934 # Open vending machine via serial.
935 logging.info('Connecting to vending machine using serial')
936 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
937 rfh,wfh = serialclient.get_fh()
939 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
940 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
942 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
943 sock.connect((options.host, options.port))
944 rfh = sock.makefile('r')
945 wfh = sock.makefile('w')
952 from optparse import OptionParser
954 op = OptionParser(usage="%prog [OPTION]...")
955 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')
956 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
957 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
958 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
959 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
960 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
961 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
962 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
963 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
964 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
965 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
966 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
967 options, args = op.parse_args()
970 op.error('extra command line arguments: ' + ' '.join(args))
975 'DBServer': ('Database', 'Server'),
976 'DBName': ('Database', 'Name'),
977 'DBUser': ('VendingMachine', 'DBUser'),
978 'DBPassword': ('VendingMachine', 'DBPassword'),
980 'ServiceName': ('VendingMachine', 'ServiceName'),
981 'ServicePassword': ('VendingMachine', 'Password'),
983 'ServerName': ('DecServer', 'Name'),
984 'ConnectPassword': ('DecServer', 'ConnectPassword'),
985 'PrivPassword': ('DecServer', 'PrivPassword'),
988 class VendConfigFile:
989 def __init__(self, config_file, options):
991 cp = ConfigParser.ConfigParser()
994 for option in options:
995 section, name = options[option]
996 value = cp.get(section, name)
997 self.__dict__[option] = value
999 except ConfigParser.Error, e:
1000 raise SystemExit("Error reading config file "+config_file+": " + str(e))
1002 def create_pid_file(name):
1004 pid_file = file(name, 'w')
1005 pid_file.write('%d\n'%os.getpid())
1008 logging.warning('unable to write to pid file '+name+': '+str(e))
1011 def do_nothing(signum, stack):
1012 signal.signal(signum, do_nothing)
1013 def stop_server(signum, stack): raise KeyboardInterrupt
1014 signal.signal(signal.SIGHUP, do_nothing)
1015 signal.signal(signal.SIGTERM, stop_server)
1016 signal.signal(signal.SIGINT, stop_server)
1018 options = parse_args()
1019 config_opts = VendConfigFile(options.config_file, config_options)
1020 if options.daemon: become_daemon()
1021 set_up_logging(options)
1022 if options.pid_file != '': create_pid_file(options.pid_file)
1024 return options, config_opts
1026 def clean_up_nicely(options, config_opts):
1027 if options.pid_file != '':
1029 os.unlink(options.pid_file)
1030 logging.debug('Removed pid file '+options.pid_file)
1031 except OSError: pass # if we can't delete it, meh
1033 def set_up_logging(options):
1034 logger = logging.getLogger()
1036 if not options.daemon:
1037 stderr_logger = logging.StreamHandler(sys.stderr)
1038 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1039 logger.addHandler(stderr_logger)
1041 if options.log_file != '':
1043 file_logger = logging.FileHandler(options.log_file)
1044 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1045 logger.addHandler(file_logger)
1047 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1049 if options.syslog != None:
1050 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1051 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1052 logger.addHandler(sys_logger)
1055 logger.setLevel(logging.WARNING)
1056 elif options.verbose:
1057 logger.setLevel(logging.DEBUG)
1059 logger.setLevel(logging.INFO)
1061 def become_daemon():
1062 dev_null = file('/dev/null')
1063 fd = dev_null.fileno()
1072 raise SystemExit('failed to fork: '+str(e))
1074 def do_vend_server(options, config_opts):
1077 rfh, wfh = connect_to_vend(options, config_opts)
1078 except (SerialClientException, socket.error), e:
1079 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1081 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1082 logging.info("Trying again in 5 seconds.")
1086 # run_forever(rfh, wfh, options, config_opts)
1089 run_forever(rfh, wfh, options, config_opts)
1090 except VendingException:
1091 logging.error("Connection died, trying again...")
1092 logging.info("Trying again in 5 seconds.")
1095 if __name__ == '__main__':
1096 options, config_opts = set_stuff_up()
1099 logging.warning('Starting Vend Server')
1100 do_vend_server(options, config_opts)
1101 logging.error('Vend Server finished unexpectedly, restarting')
1102 except KeyboardInterrupt:
1103 logging.info("Killed by signal, cleaning up")
1104 clean_up_nicely(options, config_opts)
1105 logging.warning("Vend Server stopped")
1110 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1111 tb = format_tb(exc_traceback, 20)
1114 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1115 logging.critical("Message: " + str(exc_value))
1116 logging.critical("Traceback:")
1118 for line in event.split('\n'):
1119 logging.critical(' '+line)
1120 logging.critical("This message should be considered a bug in the Vend Server.")
1121 logging.critical("Please report this to someone who can fix it.")
1123 logging.warning("Trying again anyway (might not help, but hey...)")