7 import sys, os, string, re, pwd, signal, math, syslog
8 import logging, logging.handlers
9 from traceback import format_tb
11 from time import time, sleep, mktime, localtime
12 from popen2 import popen2
13 from LATClient import LATClient, LATClientException
14 from SerialClient import SerialClient, SerialClientException
15 from VendingMachine import VendingMachine, VendingException
16 from MessageKeeper import MessageKeeper
17 from HorizScroll import HorizScroll
18 from random import random, seed
19 from Idler import GreetingIdler,TrainIdler,GrayIdler,StringIdler,ClockIdler,FortuneIdler,FileIdler,PipeIdler
20 from SnackConfig import get_snacks, get_snack
22 from posix import geteuid
25 This vending machine software brought to you by:
30 and a collective of hungry alpacas.
34 For a good time call +61 8 6488 3901
56 STATE_GRANDFATHER_CLOCK,
62 class DispenseDatabaseException(Exception): pass
64 class DispenseDatabase:
65 def __init__(self, vending_machine, host, name, user, password):
66 self.vending_machine = vending_machine
67 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
68 self.db.query('LISTEN vend_requests')
70 def process_requests(self):
71 logging.debug('database processing')
72 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
74 outstanding = self.db.query(query).getresult()
75 except (pg.error,), db_err:
76 raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
77 for (id, slot) in outstanding:
78 (worked, code, string) = self.vending_machine.vend(slot)
79 logging.debug (str((worked, code, string)))
81 query = 'SELECT vend_success(%s)'%id
82 self.db.query(query).getresult()
84 query = 'SELECT vend_failed(%s)'%id
85 self.db.query(query).getresult()
87 def handle_events(self):
88 notifier = self.db.getnotify()
89 while notifier is not None:
90 self.process_requests()
91 notify = self.db.getnotify()
93 def scroll_options(username, mk, welcome = False):
95 # Balance checking: crap code, [DAA]'s fault
96 acct = os.popen('dispense acct %s' % username)
97 balance = acct.read()[15:22]
100 msg = [(center('WELCOME'), False, TEXT_SPEED),
101 (center(username), False, TEXT_SPEED),
102 (center(balance), False, TEXT_SPEED),]
105 choices = ' '*10+'CHOICES: '
107 coke_machine = file('/home/other/coke/coke_contents')
108 cokes = coke_machine.readlines()
115 (slot_num, price, slot_name) = c.split(' ', 2)
116 if slot_name == 'dead': continue
117 choices += '%s-(%sc)-%s8 '%(slot_name, price, slot_num)
119 # we don't want to print snacks for now since it'll be too large
120 # and there's physical bits of paper in the machine anyway - matt
122 # snacks = get_snacks()
126 # for slot, ( name, price ) in snacks.items():
127 # choices += '%s8-%s (%sc) ' % ( slot, name, price )
129 choices += '55-DOOR '
130 choices += 'OR ANOTHER SNACK. '
131 choices += '99 TO READ AGAIN. '
132 choices += 'CHOICE? '
133 msg.append((choices, False, None))
138 info = pwd.getpwuid(uid)
140 logging.info('getting pin for uid %d: user not in password file'%uid)
142 if info.pw_dir == None: return False
143 pinfile = os.path.join(info.pw_dir, '.pin')
147 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
150 logging.info('getting pin for uid %d: .pin has wrong permissions. Fixing.'%uid)
151 os.chmod(pinfile, 0600)
155 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
157 pinstr = f.readline()
159 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
160 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
164 def has_good_pin(uid):
165 return get_pin(uid) != None
167 def verify_user_pin(uid, pin, skip_pin_check=False):
168 if skip_pin_check or get_pin(uid) == pin:
169 info = pwd.getpwuid(uid)
170 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
173 logging.info('refused pin for uid %d'%(uid))
179 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
180 choice = int(random()*len(messages))
181 msg = messages[choice]
182 left = range(len(msg))
183 for i in range(len(msg)):
184 if msg[i] == ' ': left.remove(i)
188 for i in range(0, len(msg)):
194 s += chr(int(random()*26)+ord('A'))
203 return ' '*((LEN-len(str))/2)+str
214 StringIdler(v, text="Kill 'em all", repeat=False),
215 GrayIdler(v,one="*",zero="-"),
216 StringIdler(v, text=CREDITS),
217 GrayIdler(v,one="/",zero="\\"),
219 GrayIdler(v,one="X",zero="O"),
220 FileIdler(v, '/usr/share/common-licenses/GPL-2',affinity=2),
221 GrayIdler(v,one="*",zero="-",reorder=1),
222 StringIdler(v, text=str(math.pi) + " "),
224 GrayIdler(v,one="/",zero="\\",reorder=1),
225 StringIdler(v, text=str(math.e) + " "),
226 GrayIdler(v,one="X",zero="O",reorder=1),
227 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),
228 PipeIdler(v, "/usr/bin/getent", "passwd"),
237 def reset_idler(v, vstatus, t = None):
239 idler = GreetingIdler(v, t)
240 vstatus.time_of_next_idlestep = time()+idler.next()
241 vstatus.time_of_next_idler = None
242 vstatus.time_to_autologout = None
243 vstatus.change_state(STATE_IDLE, 1)
248 average_affinity = 10 # guessing here...
250 if idler and idler.__class__ != GreetingIdler:
251 iiindex = idlers.index(idler)
255 move = int(random()*len(idlers)*average_affinity) + 1
260 idler = idlers[iiindex]
261 move -= idler.affinity()
265 def idle_step(vstatus):
269 vstatus.time_of_next_idler = time() + 30
270 nextidle = idler.next()
272 nextidle = IDLE_SPEED
273 vstatus.time_of_next_idlestep = time()+nextidle
276 def __init__(self,v):
277 self.state_table = {}
278 self.state = STATE_IDLE
281 self.mk = MessageKeeper(v)
285 self.cur_selection = ''
286 self.time_to_autologout = None
288 self.last_timeout_refresh = None
290 def change_state(self,newstate,newcounter=None):
291 if self.state != newstate:
292 #print "Changing state from: ",
296 self.state = newstate
298 if newcounter is not None and self.counter != newcounter:
299 #print "Changing counter from: ",
303 self.counter = newcounter
307 def handle_tick_event(event, params, v, vstatus):
308 # don't care right now.
311 def handle_switch_event(event, params, v, vstatus):
312 # don't care right now.
316 def do_nothing(state, event, params, v, vstatus):
317 print "doing nothing (s,e,p)", state, " ", event, " ", params
320 def handle_getting_uid_idle(state, event, params, v, vstatus):
321 # don't care right now.
324 def handle_getting_pin_idle(state, event, params, v, vstatus):
325 # don't care right now.
328 def handle_get_selection_idle(state, event, params, v, vstatus):
329 # don't care right now.
331 ### State logging out ..
332 if vstatus.time_to_autologout != None:
333 time_left = vstatus.time_to_autologout - time()
334 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
335 vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
336 vstatus.last_timeout_refresh = int(time_left)
337 vstatus.cur_selection = ''
339 if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
340 vstatus.time_to_autologout = None
341 vstatus.cur_user = ''
343 vstatus.cur_selection = ''
345 reset_idler(v, vstatus)
347 ### State fully logged out ... reset variables
348 if vstatus.time_to_autologout and not vstatus.mk.done():
349 vstatus.time_to_autologout = None
350 if vstatus.cur_user == '' and vstatus.time_to_autologout:
351 vstatus.time_to_autologout = None
354 if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
356 vstatus.time_to_autologout = time() + 15
357 vstatus.last_timeout_refresh = None
359 ## FIXME - this may need to be elsewhere.....
361 vstatus.mk.update_display()
365 def handle_get_selection_key(state, event, params, v, vstatus):
367 if len(vstatus.cur_selection) == 0:
370 vstatus.cur_user = ''
371 vstatus.cur_selection = ''
373 vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
374 reset_idler(v, vstatus, 2)
376 vstatus.cur_selection += chr(key + ord('0'))
377 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
378 vstatus.time_to_autologout = None
379 elif len(vstatus.cur_selection) == 1:
381 vstatus.cur_selection = ''
382 vstatus.time_to_autologout = None
383 scroll_options(vstatus.username, vstatus.mk)
386 vstatus.cur_selection += chr(key + ord('0'))
388 make_selection(v,vstatus)
389 vstatus.cur_selection = ''
390 vstatus.time_to_autologout = time() + 8
391 vstatus.last_timeout_refresh = None
394 price_check(v,vstatus)
395 vstatus.cur_selection = ''
396 vstatus.time_to_autologout = None
397 vstatus.last_timeout_refresh = None
399 def make_selection(v, vstatus):
400 # should use sudo here
401 if vstatus.cur_selection == '55':
402 vstatus.mk.set_message('OPENSESAME')
403 logging.info('dispensing a door for %s'%vstatus.username)
405 ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
407 ret = os.system('dispense door')
409 logging.info('door opened')
410 vstatus.mk.set_message(center('DOOR OPEN'))
412 logging.warning('user %s tried to dispense a bad door'%vstatus.username)
413 vstatus.mk.set_message(center('BAD DOOR'))
415 elif vstatus.cur_selection == '81':
417 elif vstatus.cur_selection == '99':
418 scroll_options(vstatus.username, vstatus.mk)
419 vstatus.cur_selection = ''
421 elif vstatus.cur_selection[1] == '8':
422 v.display('GOT DRINK?')
423 if ((os.system('su - "%s" -c "dispense %s"'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
424 v.display('SEEMS NOT')
426 v.display('GOT DRINK!')
428 # first see if it's a named slot
430 price, shortname, name = get_snack( vstatus.cur_selection )
432 price, shortname, name = get_snack( '--' )
433 dollarprice = "$%.2f" % ( price / 100.0 )
434 v.display(vstatus.cur_selection+' - %s'%dollarprice)
435 exitcode = os.system('su - "%s" -c "dispense give oday %d"'%(vstatus.username, price)) >> 8
437 # magic dispense syslog service
438 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
439 v.vend(vstatus.cur_selection)
440 v.display('THANK YOU')
442 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
443 v.display('NO MONEY?')
447 def price_check(v, vstatus):
448 if vstatus.cur_selection[1] == '8':
449 v.display(center('SEE COKE'))
451 # first see if it's a named slot
453 price, shortname, name = get_snack( vstatus.cur_selection )
455 price, shortname, name = get_snack( '--' )
456 dollarprice = "$%.2f" % ( price / 100.0 )
457 v.display(vstatus.cur_selection+' - %s'%dollarprice)
460 def handle_getting_pin_key(state, event, params, v, vstatus):
461 #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
463 if len(vstatus.cur_pin) < PIN_LENGTH:
465 if vstatus.cur_pin == '':
466 vstatus.cur_user = ''
467 reset_idler(v, vstatus)
471 vstatus.mk.set_message('PIN: ')
473 vstatus.cur_pin += chr(key + ord('0'))
474 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
475 if len(vstatus.cur_pin) == PIN_LENGTH:
476 vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
479 vstatus.cur_selection = ''
480 vstatus.change_state(STATE_GET_SELECTION)
481 scroll_options(vstatus.username, vstatus.mk, True)
485 vstatus.mk.set_messages(
486 [(center('BAD PIN'), False, 1.0),
487 (center('SORRY'), False, 0.5)])
488 vstatus.cur_user = ''
491 reset_idler(v, vstatus, 2)
496 def handle_getting_uid_key(state, event, params, v, vstatus):
497 #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
500 # complicated key handling here:
502 if len(vstatus.cur_user) == 0 and key == 9:
503 vstatus.cur_selection = ''
504 vstatus.time_to_autologout = None
505 vstatus.mk.set_message('PRICECHECK')
507 scroll_options('', vstatus.mk)
508 vstatus.change_state(STATE_GET_SELECTION)
511 if len(vstatus.cur_user) <8:
513 vstatus.cur_user = ''
515 reset_idler(v, vstatus)
517 vstatus.cur_user += chr(key + ord('0'))
518 #logging.info('dob: '+vstatus.cur_user)
519 if len(vstatus.cur_user) > 5:
520 vstatus.mk.set_message('>'+vstatus.cur_user)
522 vstatus.mk.set_message('UID: '+vstatus.cur_user)
524 if len(vstatus.cur_user) == 5:
525 uid = int(vstatus.cur_user)
528 logging.info('user '+vstatus.cur_user+' has a bad PIN')
534 Welcome to Picklevision Sytems, Sunnyvale, CA
536 Greetings Professor Falken.
541 Shall we play a game?
544 Please choose from the following menu:
551 6. Toxic and Biochemical Warfare
552 7. Global Thermonuclear War
556 Wouldn't you prefer a nice game of chess?
558 """.replace('\n',' ')
559 vstatus.mk.set_messages([(pfalken, False, 10)])
560 vstatus.cur_user = ''
563 reset_idler(v, vstatus, 10)
567 if not has_good_pin(uid):
568 logging.info('user '+vstatus.cur_user+' has a bad PIN')
569 vstatus.mk.set_messages(
570 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
571 vstatus.cur_user = ''
574 reset_idler(v, vstatus, 3)
580 vstatus.mk.set_message('PIN: ')
581 logging.info('need pin for user %s'%vstatus.cur_user)
582 vstatus.change_state(STATE_GETTING_PIN)
586 def handle_idle_key(state, event, params, v, vstatus):
587 #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
592 vstatus.cur_user = ''
593 reset_idler(v, vstatus)
596 vstatus.change_state(STATE_GETTING_UID)
597 run_handler(event, key, v, vstatus)
600 def handle_idle_tick(state, event, params, v, vstatus):
602 if vstatus.mk.done():
605 if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
606 vstatus.time_of_next_idler = time() + 30
611 vstatus.mk.update_display()
613 vstatus.change_state(STATE_GRANDFATHER_CLOCK)
614 run_handler(event, params, v, vstatus)
617 def beep_on(when, before=0):
618 start = int(when - before)
622 if now >= start and now <= end:
626 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
627 ### check for interesting times
630 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
631 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
632 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
633 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
635 hourfromnow = localtime(time() + 3600)
637 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
638 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
639 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
641 ## check for X seconds to the hour
642 ## if case, update counter to 2
643 if beep_on(onthehour,15) \
644 or beep_on(halfhour,0) \
645 or beep_on(quarterhour,0) \
646 or beep_on(threequarterhour,0) \
647 or beep_on(fivetothehour,0):
648 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
649 run_handler(event, params, v, vstatus)
651 vstatus.change_state(STATE_IDLE)
653 def handle_grandfather_tick(state, event, params, v, vstatus):
657 ### we live in interesting times
660 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
661 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
662 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
663 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
665 hourfromnow = localtime(time() + 3600)
667 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
668 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
669 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
672 #print "when it fashionable to wear a onion on your hip"
674 if beep_on(onthehour,15):
676 next_hour=((hourfromnow[3] + 11) % 12) + 1
677 if onthehour - time() < next_hour and onthehour - time() > 0:
682 msg.append(("DING!", False, None))
684 msg.append((" DING!", False, None))
685 elif int(onthehour - time()) == 0:
687 msg.append((" BONG!", False, None))
688 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
689 elif beep_on(halfhour,0):
692 msg.append((" HALFHOUR ", False, 50))
693 elif beep_on(quarterhour,0):
696 msg.append((" QTR HOUR ", False, 50))
697 elif beep_on(threequarterhour,0):
700 msg.append((" 3 QTR HR ", False, 50))
701 elif beep_on(fivetothehour,0):
704 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
708 ## check for X seconds to the hour
711 vstatus.mk.set_messages(msg)
714 vstatus.mk.update_display()
715 ## if no longer case, return to idle
717 ## change idler to be clock
718 if go_idle and vstatus.mk.done():
719 vstatus.change_state(STATE_IDLE,1)
721 def handle_door_idle(state, event, params, v, vstatus):
722 def twiddle(clock,v,wise = 2):
724 v.display("-FEED ME-")
725 elif (clock % 4 == 1+wise):
726 v.display("\\FEED ME/")
727 elif (clock % 4 == 2):
728 v.display("-FEED ME-")
729 elif (clock % 4 == 3-wise):
730 v.display("/FEED ME\\")
732 # don't care right now.
735 if ((now % 60 % 2) == 0):
738 twiddle(now, v, wise=0)
741 def handle_door_event(state, event, params, v, vstatus):
742 if params == 0: #door open
743 vstatus.change_state(STATE_DOOR_OPENING)
744 logging.warning("Entering open door mode")
745 v.display("-FEED ME-")
747 vstatus.cur_user = ''
749 elif params == 1: #door closed
750 vstatus.change_state(STATE_DOOR_CLOSING)
751 reset_idler(v, vstatus, 3)
753 logging.warning('Leaving open door mode')
754 v.display("-YUM YUM!-")
756 def handle_mifare_event(state, event, params, v, vstatus):
758 # Translate card_id into uid.
759 vstatus.cur_user = str(card_uid)
760 vstatus.username = verify_user_pin(int(card_uid), None, True)
763 vstatus.cur_selection = ''
764 vstatus.change_state(STATE_GET_SELECTION)
765 scroll_options(vstatus.username, vstatus.mk, True)
769 vstatus.mk.set_messages(
770 [(center('BAD CARD'), False, 1.0),
771 (center('SORRY'), False, 0.5)])
772 vstatus.cur_user = ''
775 reset_idler(v, vstatus, 2)
778 def return_to_idle(state,event,params,v,vstatus):
779 reset_idler(v, vstatus)
781 def create_state_table(vstatus):
782 vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
783 vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
784 vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
785 vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
787 vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
788 vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
789 vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
790 vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
792 vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
793 vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
794 vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
795 vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
797 vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
798 vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
799 vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
800 vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
802 vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
803 vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
804 vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
805 vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
807 vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
808 vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
809 vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
810 vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = do_nothing
812 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
813 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
814 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
815 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
816 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
817 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
818 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
820 def get_state_table_handler(vstatus, state, event, counter):
821 return vstatus.state_table[(state,event,counter)]
823 def time_to_next_update(vstatus):
824 idle_update = vstatus.time_of_next_idlestep - time()
825 if not vstatus.mk.done() and vstatus.mk.next_update is not None:
826 mk_update = vstatus.mk.next_update - time()
827 if mk_update < idle_update:
828 idle_update = mk_update
831 def run_forever(rfh, wfh, options, cf):
832 v = VendingMachine(rfh, wfh)
833 vstatus = VendState(v)
834 create_state_table(vstatus)
836 logging.debug('PING is ' + str(v.ping()))
838 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
841 reset_idler(v, vstatus)
843 # This main loop was hideous and the work of the devil.
844 # This has now been fixed (mostly) - mtearle
847 # notes for later surgery
848 # (event, counter, ' ')
852 # ( return state - not currently implemented )
858 except DispenseDatabaseException, e:
859 logging.error('Database error: '+str(e))
861 timeout = time_to_next_update(vstatus)
862 e = v.next_event(timeout)
865 run_handler(event, params, v, vstatus)
867 # logging.debug('Got event: ' + repr(e))
870 def run_handler(event, params, v, vstatus):
871 handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
873 handler(vstatus.state, event, params, v, vstatus)
875 def connect_to_vend(options, cf):
878 logging.info('Connecting to vending machine using LAT')
879 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
880 rfh, wfh = latclient.get_fh()
881 elif options.use_serial:
882 # Open vending machine via serial.
883 logging.info('Connecting to vending machine using serial')
884 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
885 rfh,wfh = serialclient.get_fh()
887 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
888 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
890 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
891 sock.connect((options.host, options.port))
892 rfh = sock.makefile('r')
893 wfh = sock.makefile('w')
898 from optparse import OptionParser
900 op = OptionParser(usage="%prog [OPTION]...")
901 op.add_option('-f', '--config-file', default='/etc/dispense/servers.conf', metavar='FILE', dest='config_file', help='use the specified config file instead of /etc/dispense/servers.conf')
902 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
903 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
904 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
905 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
906 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
907 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
908 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
909 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
910 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
911 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
912 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
913 options, args = op.parse_args()
916 op.error('extra command line arguments: ' + ' '.join(args))
921 'DBServer': ('Database', 'Server'),
922 'DBName': ('Database', 'Name'),
923 'DBUser': ('VendingMachine', 'DBUser'),
924 'DBPassword': ('VendingMachine', 'DBPassword'),
926 'ServiceName': ('VendingMachine', 'ServiceName'),
927 'ServicePassword': ('VendingMachine', 'Password'),
929 'ServerName': ('DecServer', 'Name'),
930 'ConnectPassword': ('DecServer', 'ConnectPassword'),
931 'PrivPassword': ('DecServer', 'PrivPassword'),
934 class VendConfigFile:
935 def __init__(self, config_file, options):
937 cp = ConfigParser.ConfigParser()
940 for option in options:
941 section, name = options[option]
942 value = cp.get(section, name)
943 self.__dict__[option] = value
945 except ConfigParser.Error, e:
946 raise SystemExit("Error reading config file "+config_file+": " + str(e))
948 def create_pid_file(name):
950 pid_file = file(name, 'w')
951 pid_file.write('%d\n'%os.getpid())
954 logging.warning('unable to write to pid file '+name+': '+str(e))
957 def do_nothing(signum, stack):
958 signal.signal(signum, do_nothing)
959 def stop_server(signum, stack): raise KeyboardInterrupt
960 signal.signal(signal.SIGHUP, do_nothing)
961 signal.signal(signal.SIGTERM, stop_server)
962 signal.signal(signal.SIGINT, stop_server)
964 options = parse_args()
965 config_opts = VendConfigFile(options.config_file, config_options)
966 if options.daemon: become_daemon()
967 set_up_logging(options)
968 if options.pid_file != '': create_pid_file(options.pid_file)
970 return options, config_opts
972 def clean_up_nicely(options, config_opts):
973 if options.pid_file != '':
975 os.unlink(options.pid_file)
976 logging.debug('Removed pid file '+options.pid_file)
977 except OSError: pass # if we can't delete it, meh
979 def set_up_logging(options):
980 logger = logging.getLogger()
982 if not options.daemon:
983 stderr_logger = logging.StreamHandler(sys.stderr)
984 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
985 logger.addHandler(stderr_logger)
987 if options.log_file != '':
989 file_logger = logging.FileHandler(options.log_file)
990 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
991 logger.addHandler(file_logger)
993 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
995 if options.syslog != None:
996 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
997 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
998 logger.addHandler(sys_logger)
1001 logger.setLevel(logging.WARNING)
1002 elif options.verbose:
1003 logger.setLevel(logging.DEBUG)
1005 logger.setLevel(logging.INFO)
1007 def become_daemon():
1008 dev_null = file('/dev/null')
1009 fd = dev_null.fileno()
1018 raise SystemExit('failed to fork: '+str(e))
1020 def do_vend_server(options, config_opts):
1023 rfh, wfh = connect_to_vend(options, config_opts)
1024 except (SerialClientException, socket.error), e:
1025 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1027 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1028 logging.info("Trying again in 5 seconds.")
1033 run_forever(rfh, wfh, options, config_opts)
1034 except VendingException:
1035 logging.error("Connection died, trying again...")
1036 logging.info("Trying again in 5 seconds.")
1039 if __name__ == '__main__':
1040 options, config_opts = set_stuff_up()
1043 logging.warning('Starting Vend Server')
1044 do_vend_server(options, config_opts)
1045 logging.error('Vend Server finished unexpectedly, restarting')
1046 except KeyboardInterrupt:
1047 logging.info("Killed by signal, cleaning up")
1048 clean_up_nicely(options, config_opts)
1049 logging.warning("Vend Server stopped")
1054 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1055 tb = format_tb(exc_traceback, 20)
1058 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1059 logging.critical("Message: " + str(exc_value))
1060 logging.critical("Traceback:")
1062 for line in event.split('\n'):
1063 logging.critical(' '+line)
1064 logging.critical("This message should be considered a bug in the Vend Server.")
1065 logging.critical("Please report this to someone who can fix it.")
1067 logging.warning("Trying again anyway (might not help, but hey...)")