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
55 STATE_GRANDFATHER_CLOCK,
61 class DispenseDatabaseException(Exception): pass
63 class DispenseDatabase:
64 def __init__(self, vending_machine, host, name, user, password):
65 self.vending_machine = vending_machine
66 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
67 self.db.query('LISTEN vend_requests')
69 def process_requests(self):
70 logging.debug('database processing')
71 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
73 outstanding = self.db.query(query).getresult()
74 except (pg.error,), db_err:
75 raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
76 for (id, slot) in outstanding:
77 (worked, code, string) = self.vending_machine.vend(slot)
78 logging.debug (str((worked, code, string)))
80 query = 'SELECT vend_success(%s)'%id
81 self.db.query(query).getresult()
83 query = 'SELECT vend_failed(%s)'%id
84 self.db.query(query).getresult()
86 def handle_events(self):
87 notifier = self.db.getnotify()
88 while notifier is not None:
89 self.process_requests()
90 notify = self.db.getnotify()
92 def scroll_options(username, mk, welcome = False):
94 msg = [(center('WELCOME'), False, TEXT_SPEED),
95 (center(username), False, TEXT_SPEED)]
98 choices = ' '*10+'CHOICES: '
100 coke_machine = file('/home/other/coke/coke_contents')
101 cokes = coke_machine.readlines()
108 (slot_num, price, slot_name) = c.split(' ', 2)
109 if slot_name == 'dead': continue
110 choices += '%s-(%sc)-%s8 '%(slot_name, price, slot_num)
112 # we don't want to print snacks for now since it'll be too large
113 # and there's physical bits of paper in the machine anyway - matt
115 # snacks = get_snacks()
119 # for slot, ( name, price ) in snacks.items():
120 # choices += '%s8-%s (%sc) ' % ( slot, name, price )
122 choices += '55-DOOR '
123 choices += 'OR ANOTHER SNACK. '
124 choices += '99 TO READ AGAIN. '
125 choices += 'CHOICE? '
126 msg.append((choices, False, None))
131 info = pwd.getpwuid(uid)
133 logging.info('getting pin for uid %d: user not in password file'%uid)
135 if info.pw_dir == None: return False
136 pinfile = os.path.join(info.pw_dir, '.pin')
140 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
143 logging.info('getting pin for uid %d: .pin has wrong permissions. Fixing.'%uid)
144 os.chmod(pinfile, 0600)
148 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
150 pinstr = f.readline()
152 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
153 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
157 def has_good_pin(uid):
158 return get_pin(uid) != None
160 def verify_user_pin(uid, pin):
161 if get_pin(uid) == pin:
162 info = pwd.getpwuid(uid)
163 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
166 logging.info('refused pin for uid %d'%(uid))
172 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
173 choice = int(random()*len(messages))
174 msg = messages[choice]
175 left = range(len(msg))
176 for i in range(len(msg)):
177 if msg[i] == ' ': left.remove(i)
181 for i in range(0, len(msg)):
187 s += chr(int(random()*26)+ord('A'))
196 return ' '*((LEN-len(str))/2)+str
207 StringIdler(v, text="Kill 'em all", repeat=False),
208 GrayIdler(v,one="*",zero="-"),
209 StringIdler(v, text=CREDITS),
210 GrayIdler(v,one="/",zero="\\"),
212 GrayIdler(v,one="X",zero="O"),
213 FileIdler(v, '/usr/share/common-licenses/GPL-2',affinity=2),
214 GrayIdler(v,one="*",zero="-",reorder=1),
215 StringIdler(v, text=str(math.pi) + " "),
217 GrayIdler(v,one="/",zero="\\",reorder=1),
218 StringIdler(v, text=str(math.e) + " "),
219 GrayIdler(v,one="X",zero="O",reorder=1),
220 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),
221 PipeIdler(v, "/usr/bin/getent", "passwd"),
230 def reset_idler(v, vstatus, t = None):
232 idler = GreetingIdler(v, t)
233 vstatus.time_of_next_idlestep = time()+idler.next()
234 vstatus.time_of_next_idler = None
235 vstatus.time_to_autologout = None
236 vstatus.change_state(STATE_IDLE, 1)
241 average_affinity = 10 # guessing here...
243 if idler and idler.__class__ != GreetingIdler:
244 iiindex = idlers.index(idler)
248 move = int(random()*len(idlers)*average_affinity) + 1
253 idler = idlers[iiindex]
254 move -= idler.affinity()
258 def idle_step(vstatus):
262 vstatus.time_of_next_idler = time() + 30
263 nextidle = idler.next()
265 nextidle = IDLE_SPEED
266 vstatus.time_of_next_idlestep = time()+nextidle
269 def __init__(self,v):
270 self.state_table = {}
271 self.state = STATE_IDLE
274 self.mk = MessageKeeper(v)
278 self.cur_selection = ''
279 self.time_to_autologout = None
281 self.last_timeout_refresh = None
283 def change_state(self,newstate,newcounter=None):
284 if self.state != newstate:
285 #print "Changing state from: ",
289 self.state = newstate
291 if newcounter is not None and self.counter != newcounter:
292 #print "Changing counter from: ",
296 self.counter = newcounter
300 def handle_tick_event(event, params, v, vstatus):
301 # don't care right now.
304 def handle_switch_event(event, params, v, vstatus):
305 # don't care right now.
309 def do_nothing(state, event, params, v, vstatus):
310 print "doing nothing (s,e,p)", state, " ", event, " ", params
313 def handle_getting_uid_idle(state, event, params, v, vstatus):
314 # don't care right now.
317 def handle_getting_pin_idle(state, event, params, v, vstatus):
318 # don't care right now.
321 def handle_get_selection_idle(state, event, params, v, vstatus):
322 # don't care right now.
324 ### State logging out ..
325 if vstatus.time_to_autologout != None:
326 time_left = vstatus.time_to_autologout - time()
327 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
328 vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
329 vstatus.last_timeout_refresh = int(time_left)
330 vstatus.cur_selection = ''
332 if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
333 vstatus.time_to_autologout = None
334 vstatus.cur_user = ''
336 vstatus.cur_selection = ''
338 reset_idler(v, vstatus)
340 ### State fully logged out ... reset variables
341 if vstatus.time_to_autologout and not vstatus.mk.done():
342 vstatus.time_to_autologout = None
343 if vstatus.cur_user == '' and vstatus.time_to_autologout:
344 vstatus.time_to_autologout = None
347 if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
349 vstatus.time_to_autologout = time() + 15
350 vstatus.last_timeout_refresh = None
352 ## FIXME - this may need to be elsewhere.....
354 vstatus.mk.update_display()
358 def handle_get_selection_key(state, event, params, v, vstatus):
360 if len(vstatus.cur_selection) == 0:
363 vstatus.cur_user = ''
364 vstatus.cur_selection = ''
366 vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
367 reset_idler(v, vstatus, 2)
369 vstatus.cur_selection += chr(key + ord('0'))
370 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
371 vstatus.time_to_autologout = None
372 elif len(vstatus.cur_selection) == 1:
374 vstatus.cur_selection = ''
375 vstatus.time_to_autologout = None
376 scroll_options(vstatus.username, vstatus.mk)
379 vstatus.cur_selection += chr(key + ord('0'))
381 make_selection(v,vstatus)
382 vstatus.cur_selection = ''
383 vstatus.time_to_autologout = time() + 8
384 vstatus.last_timeout_refresh = None
387 price_check(v,vstatus)
388 vstatus.cur_selection = ''
389 vstatus.time_to_autologout = None
390 vstatus.last_timeout_refresh = None
392 def make_selection(v, vstatus):
393 # should use sudo here
394 if vstatus.cur_selection == '55':
395 vstatus.mk.set_message('OPENSESAME')
396 logging.info('dispensing a door for %s'%vstatus.username)
398 ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
400 ret = os.system('dispense door')
402 logging.info('door opened')
403 vstatus.mk.set_message(center('DOOR OPEN'))
405 logging.warning('user %s tried to dispense a bad door'%vstatus.username)
406 vstatus.mk.set_message(center('BAD DOOR'))
408 elif vstatus.cur_selection == '81':
410 elif vstatus.cur_selection == '99':
411 scroll_options(vstatus.username, vstatus.mk)
412 vstatus.cur_selection = ''
414 elif vstatus.cur_selection[1] == '8':
415 v.display('GOT DRINK?')
416 if ((os.system('su - "%s" -c "dispense %s"'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
417 v.display('SEEMS NOT')
419 v.display('GOT DRINK!')
421 # first see if it's a named slot
423 price, shortname, name = get_snack( vstatus.cur_selection )
425 price, shortname, name = get_snack( '--' )
426 dollarprice = "$%.2f" % ( price / 100.0 )
427 v.display(vstatus.cur_selection+' - %s'%dollarprice)
428 exitcode = os.system('su - "%s" -c "dispense give oday %d"'%(vstatus.username, price)) >> 8
430 # magic dispense syslog service
431 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
432 v.vend(vstatus.cur_selection)
433 v.display('THANK YOU')
435 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
436 v.display('NO MONEY?')
440 def price_check(v, vstatus):
441 if vstatus.cur_selection[1] == '8':
442 v.display(center('SEE COKE'))
444 # first see if it's a named slot
446 price, shortname, name = get_snack( vstatus.cur_selection )
448 price, shortname, name = get_snack( '--' )
449 dollarprice = "$%.2f" % ( price / 100.0 )
450 v.display(vstatus.cur_selection+' - %s'%dollarprice)
453 def handle_getting_pin_key(state, event, params, v, vstatus):
454 #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
456 if len(vstatus.cur_pin) < PIN_LENGTH:
458 if vstatus.cur_pin == '':
459 vstatus.cur_user = ''
460 reset_idler(v, vstatus)
464 vstatus.mk.set_message('PIN: ')
466 vstatus.cur_pin += chr(key + ord('0'))
467 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
468 if len(vstatus.cur_pin) == PIN_LENGTH:
469 vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
472 vstatus.cur_selection = ''
473 vstatus.change_state(STATE_GET_SELECTION)
474 scroll_options(vstatus.username, vstatus.mk, True)
478 vstatus.mk.set_messages(
479 [(center('BAD PIN'), False, 1.0),
480 (center('SORRY'), False, 0.5)])
481 vstatus.cur_user = ''
484 reset_idler(v, vstatus, 2)
489 def handle_getting_uid_key(state, event, params, v, vstatus):
490 #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
493 # complicated key handling here:
495 if len(vstatus.cur_user) == 0 and key == 9:
496 vstatus.cur_selection = ''
497 vstatus.time_to_autologout = None
498 vstatus.mk.set_message('PRICECHECK')
500 scroll_options('', vstatus.mk)
501 vstatus.change_state(STATE_GET_SELECTION)
504 if len(vstatus.cur_user) <8:
506 vstatus.cur_user = ''
508 reset_idler(v, vstatus)
510 vstatus.cur_user += chr(key + ord('0'))
511 #logging.info('dob: '+vstatus.cur_user)
512 if len(vstatus.cur_user) > 5:
513 vstatus.mk.set_message('>'+vstatus.cur_user)
515 vstatus.mk.set_message('UID: '+vstatus.cur_user)
518 # Easter egg for nikita's birthday -- DGB
519 if len(vstatus.cur_user) == 8:
520 if vstatus.cur_user != "07051980":
521 vstatus.mk.set_messages(
522 [(' '*9+'ONE MORE TRY NiKiTa'+' '*10, False, 3)])
523 vstatus.cur_user = ''
524 reset_idler(v, vstatus, 3)
528 vstatus.mk.set_messages(
529 [(center(' GUILD MAILBOX NUMBER 64 '), False, 20),
530 (center(' GUILD MAILBOX NUMBER 64 '), False, 20),
531 (center(' GUILD MAILBOX NUMBER 64 '), False, 20),
532 (center(' GUILD MAILBOX NUMBER 64 '), False, 20)])
535 vstatus.cur_user = ''
537 #reset_idler(v, vstatus, 10)
538 reset_idler(v, vstatus, 2)
540 # End easter egg part 1
541 if len(vstatus.cur_user) == 5:
542 uid = int(vstatus.cur_user)
544 # Easter egg for nikita's birthday -- DGB
545 if vstatus.cur_user == '07051':
547 vstatus.cur_user = ''
548 reset_idler(v, vstatus)
550 # vstatus.cur_user += chr(key + ord('0'))
551 logging.info(' == 5 dob: '+vstatus.cur_user)
552 vstatus.mk.set_message('>'+vstatus.cur_user)
554 # end easter egg part 2
557 logging.info('user '+vstatus.cur_user+' has a bad PIN')
563 Welcome to Picklevision Sytems, Sunnyvale, CA
565 Greetings Professor Falken.
570 Shall we play a game?
573 Please choose from the following menu:
580 6. Toxic and Biochemical Warfare
581 7. Global Thermonuclear War
585 Wouldn't you prefer a nice game of chess?
587 """.replace('\n',' ')
588 vstatus.mk.set_messages([(pfalken, False, 10)])
589 vstatus.cur_user = ''
592 reset_idler(v, vstatus, 10)
596 if not has_good_pin(uid):
597 logging.info('user '+vstatus.cur_user+' has a bad PIN')
598 vstatus.mk.set_messages(
599 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
600 vstatus.cur_user = ''
603 reset_idler(v, vstatus, 3)
609 vstatus.mk.set_message('PIN: ')
610 logging.info('need pin for user %s'%vstatus.cur_user)
611 vstatus.change_state(STATE_GETTING_PIN)
615 def handle_idle_key(state, event, params, v, vstatus):
616 #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
621 vstatus.cur_user = ''
622 reset_idler(v, vstatus)
625 vstatus.change_state(STATE_GETTING_UID)
626 run_handler(event, key, v, vstatus)
629 def handle_idle_tick(state, event, params, v, vstatus):
631 if vstatus.mk.done():
634 if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
635 vstatus.time_of_next_idler = time() + 30
640 vstatus.mk.update_display()
642 vstatus.change_state(STATE_GRANDFATHER_CLOCK)
643 run_handler(event, params, v, vstatus)
646 def beep_on(when, before=0):
647 start = int(when - before)
651 if now >= start and now <= end:
655 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
656 ### check for interesting times
659 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
660 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
661 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
662 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
664 hourfromnow = localtime(time() + 3600)
666 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
667 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
668 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
670 ## check for X seconds to the hour
671 ## if case, update counter to 2
672 if beep_on(onthehour,15) \
673 or beep_on(halfhour,0) \
674 or beep_on(quarterhour,0) \
675 or beep_on(threequarterhour,0) \
676 or beep_on(fivetothehour,0):
677 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
678 run_handler(event, params, v, vstatus)
680 vstatus.change_state(STATE_IDLE)
682 def handle_grandfather_tick(state, event, params, v, vstatus):
686 ### we live in interesting times
689 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
690 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
691 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
692 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
694 hourfromnow = localtime(time() + 3600)
696 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
697 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
698 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
701 #print "when it fashionable to wear a onion on your hip"
703 if beep_on(onthehour,15):
705 next_hour=((hourfromnow[3] + 11) % 12) + 1
706 if onthehour - time() < next_hour and onthehour - time() > 0:
711 msg.append(("DING!", False, None))
713 msg.append((" DING!", False, None))
714 elif int(onthehour - time()) == 0:
716 msg.append((" BONG!", False, None))
717 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
718 elif beep_on(halfhour,0):
721 msg.append((" HALFHOUR ", False, 50))
722 elif beep_on(quarterhour,0):
725 msg.append((" QTR HOUR ", False, 50))
726 elif beep_on(threequarterhour,0):
729 msg.append((" 3 QTR HR ", False, 50))
730 elif beep_on(fivetothehour,0):
733 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
737 ## check for X seconds to the hour
740 vstatus.mk.set_messages(msg)
743 vstatus.mk.update_display()
744 ## if no longer case, return to idle
746 ## change idler to be clock
747 if go_idle and vstatus.mk.done():
748 vstatus.change_state(STATE_IDLE,1)
750 def handle_door_idle(state, event, params, v, vstatus):
751 def twiddle(clock,v,wise = 2):
753 v.display("-FEED ME-")
754 elif (clock % 4 == 1+wise):
755 v.display("\\FEED ME/")
756 elif (clock % 4 == 2):
757 v.display("-FEED ME-")
758 elif (clock % 4 == 3-wise):
759 v.display("/FEED ME\\")
761 # don't care right now.
764 if ((now % 60 % 2) == 0):
767 twiddle(now, v, wise=0)
770 def handle_door_event(state, event, params, v, vstatus):
771 if params == 0: #door open
772 vstatus.change_state(STATE_DOOR_OPENING)
773 logging.warning("Entering open door mode")
774 v.display("-FEED ME-")
776 vstatus.cur_user = ''
778 elif params == 1: #door closed
779 vstatus.change_state(STATE_DOOR_CLOSING)
780 reset_idler(v, vstatus, 3)
782 logging.warning('Leaving open door mode')
783 v.display("-YUM YUM!-")
785 def return_to_idle(state,event,params,v,vstatus):
786 reset_idler(v, vstatus)
788 def create_state_table(vstatus):
789 vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
790 vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
791 vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
793 vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
794 vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
795 vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
797 vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
798 vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
799 vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
801 vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
802 vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
803 vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
805 vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
806 vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
807 vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
809 vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
810 vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
811 vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
813 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
814 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
815 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
816 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
817 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
818 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
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))
862 timeout = time_to_next_update(vstatus)
863 e = v.next_event(timeout)
866 run_handler(event, params, v, vstatus)
868 # logging.debug('Got event: ' + repr(e))
871 def run_handler(event, params, v, vstatus):
872 handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
874 handler(vstatus.state, event, params, v, vstatus)
876 def connect_to_vend(options, cf):
879 logging.info('Connecting to vending machine using LAT')
880 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
881 rfh, wfh = latclient.get_fh()
882 elif options.use_serial:
883 # Open vending machine via serial.
884 logging.info('Connecting to vending machine using serial')
885 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
886 rfh,wfh = serialclient.get_fh()
888 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
889 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
891 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
892 sock.connect((options.host, options.port))
893 rfh = sock.makefile('r')
894 wfh = sock.makefile('w')
899 from optparse import OptionParser
901 op = OptionParser(usage="%prog [OPTION]...")
902 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')
903 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
904 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
905 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
906 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
907 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
908 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
909 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
910 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
911 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
912 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
913 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
914 options, args = op.parse_args()
917 op.error('extra command line arguments: ' + ' '.join(args))
922 'DBServer': ('Database', 'Server'),
923 'DBName': ('Database', 'Name'),
924 'DBUser': ('VendingMachine', 'DBUser'),
925 'DBPassword': ('VendingMachine', 'DBPassword'),
927 'ServiceName': ('VendingMachine', 'ServiceName'),
928 'ServicePassword': ('VendingMachine', 'Password'),
930 'ServerName': ('DecServer', 'Name'),
931 'ConnectPassword': ('DecServer', 'ConnectPassword'),
932 'PrivPassword': ('DecServer', 'PrivPassword'),
935 class VendConfigFile:
936 def __init__(self, config_file, options):
938 cp = ConfigParser.ConfigParser()
941 for option in options:
942 section, name = options[option]
943 value = cp.get(section, name)
944 self.__dict__[option] = value
946 except ConfigParser.Error, e:
947 raise SystemExit("Error reading config file "+config_file+": " + str(e))
949 def create_pid_file(name):
951 pid_file = file(name, 'w')
952 pid_file.write('%d\n'%os.getpid())
955 logging.warning('unable to write to pid file '+name+': '+str(e))
958 def do_nothing(signum, stack):
959 signal.signal(signum, do_nothing)
960 def stop_server(signum, stack): raise KeyboardInterrupt
961 signal.signal(signal.SIGHUP, do_nothing)
962 signal.signal(signal.SIGTERM, stop_server)
963 signal.signal(signal.SIGINT, stop_server)
965 options = parse_args()
966 config_opts = VendConfigFile(options.config_file, config_options)
967 if options.daemon: become_daemon()
968 set_up_logging(options)
969 if options.pid_file != '': create_pid_file(options.pid_file)
971 return options, config_opts
973 def clean_up_nicely(options, config_opts):
974 if options.pid_file != '':
976 os.unlink(options.pid_file)
977 logging.debug('Removed pid file '+options.pid_file)
978 except OSError: pass # if we can't delete it, meh
980 def set_up_logging(options):
981 logger = logging.getLogger()
983 if not options.daemon:
984 stderr_logger = logging.StreamHandler(sys.stderr)
985 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
986 logger.addHandler(stderr_logger)
988 if options.log_file != '':
990 file_logger = logging.FileHandler(options.log_file)
991 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
992 logger.addHandler(file_logger)
994 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
996 if options.syslog != None:
997 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
998 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
999 logger.addHandler(sys_logger)
1002 logger.setLevel(logging.WARNING)
1003 elif options.verbose:
1004 logger.setLevel(logging.DEBUG)
1006 logger.setLevel(logging.INFO)
1008 def become_daemon():
1009 dev_null = file('/dev/null')
1010 fd = dev_null.fileno()
1019 raise SystemExit('failed to fork: '+str(e))
1021 def do_vend_server(options, config_opts):
1024 rfh, wfh = connect_to_vend(options, config_opts)
1025 except (SerialClientException, socket.error), e:
1026 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1028 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1029 logging.info("Trying again in 5 seconds.")
1034 run_forever(rfh, wfh, options, config_opts)
1035 except VendingException:
1036 logging.error("Connection died, trying again...")
1037 logging.info("Trying again in 5 seconds.")
1040 if __name__ == '__main__':
1041 options, config_opts = set_stuff_up()
1044 logging.warning('Starting Vend Server')
1045 do_vend_server(options, config_opts)
1046 logging.error('Vend Server finished unexpectedly, restarting')
1047 except KeyboardInterrupt:
1048 logging.info("Killed by signal, cleaning up")
1049 clean_up_nicely(options, config_opts)
1050 logging.warning("Vend Server stopped")
1055 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1056 tb = format_tb(exc_traceback, 20)
1059 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1060 logging.critical("Message: " + str(exc_value))
1061 logging.critical("Traceback:")
1063 for line in event.split('\n'):
1064 logging.critical(' '+line)
1065 logging.critical("This message should be considered a bug in the Vend Server.")
1066 logging.critical("Please report this to someone who can fix it.")
1068 logging.warning("Trying again anyway (might not help, but hey...)")