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 popen2 import popen2
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_snacks, get_snack
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):
97 # Balance checking: crap code, [DAA]'s fault
98 # Updated 2011 to handle new dispense [MRD]
99 raw_acct = os.popen('dispense acct %s' % username)
100 acct = raw_acct.read()
101 # this is fucking appalling
102 balance = acct[acct.find("$")+1:acct.find("(")].strip()
105 msg = [(center('WELCOME'), False, TEXT_SPEED),
106 (center(username), False, TEXT_SPEED),
107 (center(balance), False, TEXT_SPEED),]
110 choices = ' '*10+'CHOICES: '
112 coke_machine = file('/home/other/coke/coke_contents')
113 cokes = coke_machine.readlines()
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)
415 ret = os.system('dispense door')
417 logging.info('door opened')
418 vstatus.mk.set_message(center('DOOR OPEN'))
420 logging.warning('user %s tried to dispense a bad door'%vstatus.username)
421 vstatus.mk.set_message(center('BAD DOOR'))
423 elif vstatus.cur_selection == '81':
425 elif vstatus.cur_selection == '99':
426 scroll_options(vstatus.username, vstatus.mk)
427 vstatus.cur_selection = ''
429 elif vstatus.cur_selection[1] == '8':
430 v.display('GOT DRINK?')
431 if ((os.system('su - "%s" -c "dispense %s"'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
432 v.display('SEEMS NOT')
434 v.display('GOT DRINK!')
436 # first see if it's a named slot
438 price, shortname, name = get_snack( vstatus.cur_selection )
440 price, shortname, name = get_snack( '--' )
441 dollarprice = "$%.2f" % ( price / 100.0 )
442 v.display(vstatus.cur_selection+' - %s'%dollarprice)
443 exitcode = os.system('su - "%s" -c "dispense give \>snacksales %d \'%s\'"'%(vstatus.username, price, name)) >> 8
445 # magic dispense syslog service
446 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
447 v.vend(vstatus.cur_selection)
448 v.display('THANK YOU')
450 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
451 v.display('NO MONEY?')
455 def price_check(v, vstatus):
456 if vstatus.cur_selection[1] == '8':
457 v.display(center('SEE COKE'))
459 # first see if it's a named slot
461 price, shortname, name = get_snack( vstatus.cur_selection )
463 price, shortname, name = get_snack( '--' )
464 dollarprice = "$%.2f" % ( price / 100.0 )
465 v.display(vstatus.cur_selection+' - %s'%dollarprice)
468 def handle_getting_pin_key(state, event, params, v, vstatus):
469 #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
471 if len(vstatus.cur_pin) < PIN_LENGTH:
473 if vstatus.cur_pin == '':
474 vstatus.cur_user = ''
475 reset_idler(v, vstatus)
479 vstatus.mk.set_message('PIN: ')
481 vstatus.cur_pin += chr(key + ord('0'))
482 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
483 if len(vstatus.cur_pin) == PIN_LENGTH:
484 vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
487 vstatus.cur_selection = ''
488 vstatus.change_state(STATE_GET_SELECTION)
489 scroll_options(vstatus.username, vstatus.mk, True)
493 vstatus.mk.set_messages(
494 [(center('BAD PIN'), False, 1.0),
495 (center('SORRY'), False, 0.5)])
496 vstatus.cur_user = ''
499 reset_idler(v, vstatus, 2)
504 def handle_getting_uid_key(state, event, params, v, vstatus):
505 #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
508 # complicated key handling here:
510 if len(vstatus.cur_user) == 0 and key == 9:
511 vstatus.cur_selection = ''
512 vstatus.time_to_autologout = None
513 vstatus.mk.set_message('PRICECHECK')
515 scroll_options('', vstatus.mk)
516 vstatus.change_state(STATE_GET_SELECTION)
519 if len(vstatus.cur_user) <8:
521 vstatus.cur_user = ''
523 reset_idler(v, vstatus)
525 vstatus.cur_user += chr(key + ord('0'))
526 #logging.info('dob: '+vstatus.cur_user)
527 if len(vstatus.cur_user) > 5:
528 vstatus.mk.set_message('>'+vstatus.cur_user)
530 vstatus.mk.set_message('UID: '+vstatus.cur_user)
532 if len(vstatus.cur_user) == 5:
533 uid = int(vstatus.cur_user)
536 logging.info('user '+vstatus.cur_user+' has a bad PIN')
542 Welcome to Picklevision Sytems, Sunnyvale, CA
544 Greetings Professor Falken.
549 Shall we play a game?
552 Please choose from the following menu:
559 6. Toxic and Biochemical Warfare
560 7. Global Thermonuclear War
564 Wouldn't you prefer a nice game of chess?
566 """.replace('\n',' ')
567 vstatus.mk.set_messages([(pfalken, False, 10)])
568 vstatus.cur_user = ''
571 reset_idler(v, vstatus, 10)
575 if not has_good_pin(uid):
576 logging.info('user '+vstatus.cur_user+' has a bad PIN')
577 vstatus.mk.set_messages(
578 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
579 vstatus.cur_user = ''
582 reset_idler(v, vstatus, 3)
588 vstatus.mk.set_message('PIN: ')
589 logging.info('need pin for user %s'%vstatus.cur_user)
590 vstatus.change_state(STATE_GETTING_PIN)
594 def handle_idle_key(state, event, params, v, vstatus):
595 #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
600 vstatus.cur_user = ''
601 reset_idler(v, vstatus)
604 vstatus.change_state(STATE_GETTING_UID)
605 run_handler(event, key, v, vstatus)
608 def handle_idle_tick(state, event, params, v, vstatus):
610 if vstatus.mk.done():
613 if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
614 vstatus.time_of_next_idler = time() + 30
619 vstatus.mk.update_display()
621 vstatus.change_state(STATE_GRANDFATHER_CLOCK)
622 run_handler(event, params, v, vstatus)
625 def beep_on(when, before=0):
626 start = int(when - before)
630 if now >= start and now <= end:
634 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
635 ### check for interesting times
638 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
639 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
640 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
641 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
643 hourfromnow = localtime(time() + 3600)
645 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
646 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
647 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
649 ## check for X seconds to the hour
650 ## if case, update counter to 2
651 if beep_on(onthehour,15) \
652 or beep_on(halfhour,0) \
653 or beep_on(quarterhour,0) \
654 or beep_on(threequarterhour,0) \
655 or beep_on(fivetothehour,0):
656 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
657 run_handler(event, params, v, vstatus)
659 vstatus.change_state(STATE_IDLE)
661 def handle_grandfather_tick(state, event, params, v, vstatus):
665 ### we live in interesting times
668 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
669 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
670 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
671 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
673 hourfromnow = localtime(time() + 3600)
675 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
676 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
677 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
680 #print "when it fashionable to wear a onion on your hip"
682 if beep_on(onthehour,15):
684 next_hour=((hourfromnow[3] + 11) % 12) + 1
685 if onthehour - time() < next_hour and onthehour - time() > 0:
690 msg.append(("DING!", False, None))
692 msg.append((" DING!", False, None))
693 elif int(onthehour - time()) == 0:
695 msg.append((" BONG!", False, None))
696 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
697 elif beep_on(halfhour,0):
700 msg.append((" HALFHOUR ", False, 50))
701 elif beep_on(quarterhour,0):
704 msg.append((" QTR HOUR ", False, 50))
705 elif beep_on(threequarterhour,0):
708 msg.append((" 3 QTR HR ", False, 50))
709 elif beep_on(fivetothehour,0):
712 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
716 ## check for X seconds to the hour
719 vstatus.mk.set_messages(msg)
722 vstatus.mk.update_display()
723 ## if no longer case, return to idle
725 ## change idler to be clock
726 if go_idle and vstatus.mk.done():
727 vstatus.change_state(STATE_IDLE,1)
729 def handle_door_idle(state, event, params, v, vstatus):
730 def twiddle(clock,v,wise = 2):
732 v.display("-FEED ME-")
733 elif (clock % 4 == 1+wise):
734 v.display("\\FEED ME/")
735 elif (clock % 4 == 2):
736 v.display("-FEED ME-")
737 elif (clock % 4 == 3-wise):
738 v.display("/FEED ME\\")
740 # don't care right now.
743 if ((now % 60 % 2) == 0):
746 twiddle(now, v, wise=0)
749 def handle_door_event(state, event, params, v, vstatus):
750 if params == 0: #door open
751 vstatus.change_state(STATE_DOOR_OPENING)
752 logging.warning("Entering open door mode")
753 v.display("-FEED ME-")
755 vstatus.cur_user = ''
757 elif params == 1: #door closed
758 vstatus.change_state(STATE_DOOR_CLOSING)
759 reset_idler(v, vstatus, 3)
761 logging.warning('Leaving open door mode')
762 v.display("-YUM YUM!-")
764 def handle_mifare_event(state, event, params, v, vstatus):
766 # Translate card_id into uid.
771 vstatus.cur_user = get_uid(card_id)
772 logging.info('Mapped card id to uid %s'%vstatus.cur_user)
773 vstatus.username = verify_user_pin(int(vstatus.cur_user), None, True)
775 vstatus.username = None
778 vstatus.cur_selection = ''
779 vstatus.change_state(STATE_GET_SELECTION)
780 scroll_options(vstatus.username, vstatus.mk, True)
784 vstatus.mk.set_messages(
785 [(center('BAD CARD'), False, 1.0),
786 (center('SORRY'), False, 0.5)])
787 vstatus.cur_user = ''
790 reset_idler(v, vstatus, 2)
793 def handle_mifare_add_user_event(state, event, params, v, vstatus):
796 # Translate card_id into uid.
801 if get_uid(card_id) != None:
802 vstatus.mk.set_messages(
803 [(center('ALREADY'), False, 0.5),
804 (center('ENROLLED'), False, 0.5)])
806 # scroll_options(vstatus.username, vstatus.mk)
811 logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
812 set_card_id(vstatus.cur_user, card_id)
813 vstatus.mk.set_messages(
814 [(center('CARD'), False, 0.5),
815 (center('ENROLLED'), False, 0.5)])
817 # scroll_options(vstatus.username, vstatus.mk)
819 def return_to_idle(state,event,params,v,vstatus):
820 reset_idler(v, vstatus)
822 def create_state_table(vstatus):
823 vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
824 vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
825 vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
826 vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
828 vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
829 vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
830 vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
831 vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
833 vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
834 vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
835 vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
836 vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
838 vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
839 vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
840 vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
841 vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
843 vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
844 vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
845 vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
846 vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
848 vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
849 vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
850 vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
851 vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
853 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
854 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
855 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
856 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
857 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
858 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
859 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
861 def get_state_table_handler(vstatus, state, event, counter):
862 return vstatus.state_table[(state,event,counter)]
864 def time_to_next_update(vstatus):
865 idle_update = vstatus.time_of_next_idlestep - time()
866 if not vstatus.mk.done() and vstatus.mk.next_update is not None:
867 mk_update = vstatus.mk.next_update - time()
868 if mk_update < idle_update:
869 idle_update = mk_update
872 def run_forever(rfh, wfh, options, cf):
873 v = VendingMachine(rfh, wfh, USE_MIFARE)
874 vstatus = VendState(v)
875 create_state_table(vstatus)
877 logging.debug('PING is ' + str(v.ping()))
879 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
882 reset_idler(v, vstatus)
884 # This main loop was hideous and the work of the devil.
885 # This has now been fixed (mostly) - mtearle
888 # notes for later surgery
889 # (event, counter, ' ')
893 # ( return state - not currently implemented )
899 except DispenseDatabaseException, e:
900 logging.error('Database error: '+str(e))
902 timeout = time_to_next_update(vstatus)
903 e = v.next_event(timeout)
906 run_handler(event, params, v, vstatus)
908 # logging.debug('Got event: ' + repr(e))
911 def run_handler(event, params, v, vstatus):
912 handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
914 handler(vstatus.state, event, params, v, vstatus)
916 def connect_to_vend(options, cf):
919 logging.info('Connecting to vending machine using LAT')
920 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
921 rfh, wfh = latclient.get_fh()
922 elif options.use_serial:
923 # Open vending machine via serial.
924 logging.info('Connecting to vending machine using serial')
925 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
926 rfh,wfh = serialclient.get_fh()
928 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
929 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
931 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
932 sock.connect((options.host, options.port))
933 rfh = sock.makefile('r')
934 wfh = sock.makefile('w')
941 from optparse import OptionParser
943 op = OptionParser(usage="%prog [OPTION]...")
944 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')
945 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
946 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
947 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
948 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
949 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
950 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
951 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
952 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
953 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
954 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
955 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
956 options, args = op.parse_args()
959 op.error('extra command line arguments: ' + ' '.join(args))
964 'DBServer': ('Database', 'Server'),
965 'DBName': ('Database', 'Name'),
966 'DBUser': ('VendingMachine', 'DBUser'),
967 'DBPassword': ('VendingMachine', 'DBPassword'),
969 'ServiceName': ('VendingMachine', 'ServiceName'),
970 'ServicePassword': ('VendingMachine', 'Password'),
972 'ServerName': ('DecServer', 'Name'),
973 'ConnectPassword': ('DecServer', 'ConnectPassword'),
974 'PrivPassword': ('DecServer', 'PrivPassword'),
977 class VendConfigFile:
978 def __init__(self, config_file, options):
980 cp = ConfigParser.ConfigParser()
983 for option in options:
984 section, name = options[option]
985 value = cp.get(section, name)
986 self.__dict__[option] = value
988 except ConfigParser.Error, e:
989 raise SystemExit("Error reading config file "+config_file+": " + str(e))
991 def create_pid_file(name):
993 pid_file = file(name, 'w')
994 pid_file.write('%d\n'%os.getpid())
997 logging.warning('unable to write to pid file '+name+': '+str(e))
1000 def do_nothing(signum, stack):
1001 signal.signal(signum, do_nothing)
1002 def stop_server(signum, stack): raise KeyboardInterrupt
1003 signal.signal(signal.SIGHUP, do_nothing)
1004 signal.signal(signal.SIGTERM, stop_server)
1005 signal.signal(signal.SIGINT, stop_server)
1007 options = parse_args()
1008 config_opts = VendConfigFile(options.config_file, config_options)
1009 if options.daemon: become_daemon()
1010 set_up_logging(options)
1011 if options.pid_file != '': create_pid_file(options.pid_file)
1013 return options, config_opts
1015 def clean_up_nicely(options, config_opts):
1016 if options.pid_file != '':
1018 os.unlink(options.pid_file)
1019 logging.debug('Removed pid file '+options.pid_file)
1020 except OSError: pass # if we can't delete it, meh
1022 def set_up_logging(options):
1023 logger = logging.getLogger()
1025 if not options.daemon:
1026 stderr_logger = logging.StreamHandler(sys.stderr)
1027 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1028 logger.addHandler(stderr_logger)
1030 if options.log_file != '':
1032 file_logger = logging.FileHandler(options.log_file)
1033 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1034 logger.addHandler(file_logger)
1036 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1038 if options.syslog != None:
1039 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1040 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1041 logger.addHandler(sys_logger)
1044 logger.setLevel(logging.WARNING)
1045 elif options.verbose:
1046 logger.setLevel(logging.DEBUG)
1048 logger.setLevel(logging.INFO)
1050 def become_daemon():
1051 dev_null = file('/dev/null')
1052 fd = dev_null.fileno()
1061 raise SystemExit('failed to fork: '+str(e))
1063 def do_vend_server(options, config_opts):
1066 rfh, wfh = connect_to_vend(options, config_opts)
1067 except (SerialClientException, socket.error), e:
1068 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1070 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1071 logging.info("Trying again in 5 seconds.")
1076 run_forever(rfh, wfh, options, config_opts)
1077 except VendingException:
1078 logging.error("Connection died, trying again...")
1079 logging.info("Trying again in 5 seconds.")
1082 if __name__ == '__main__':
1083 options, config_opts = set_stuff_up()
1086 logging.warning('Starting Vend Server')
1087 do_vend_server(options, config_opts)
1088 logging.error('Vend Server finished unexpectedly, restarting')
1089 except KeyboardInterrupt:
1090 logging.info("Killed by signal, cleaning up")
1091 clean_up_nicely(options, config_opts)
1092 logging.warning("Vend Server stopped")
1097 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1098 tb = format_tb(exc_traceback, 20)
1101 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1102 logging.critical("Message: " + str(exc_value))
1103 logging.critical("Traceback:")
1105 for line in event.split('\n'):
1106 logging.critical(' '+line)
1107 logging.critical("This message should be considered a bug in the Vend Server.")
1108 logging.critical("Please report this to someone who can fix it.")
1110 logging.warning("Trying again anyway (might not help, but hey...)")