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 acct = os.popen('dispense acct %s' % username)
99 balance = acct.read()[15:22]
102 msg = [(center('WELCOME'), False, TEXT_SPEED),
103 (center(username), False, TEXT_SPEED),
104 (center(balance), False, TEXT_SPEED),]
107 choices = ' '*10+'CHOICES: '
109 coke_machine = file('/home/other/coke/coke_contents')
110 cokes = coke_machine.readlines()
117 (slot_num, price, slot_name) = c.split(' ', 2)
118 if slot_name == 'dead': continue
119 choices += '%s-(%sc)-%s8 '%(slot_name, price, slot_num)
121 # we don't want to print snacks for now since it'll be too large
122 # and there's physical bits of paper in the machine anyway - matt
124 # snacks = get_snacks()
128 # for slot, ( name, price ) in snacks.items():
129 # choices += '%s8-%s (%sc) ' % ( slot, name, price )
131 choices += '55-DOOR '
132 choices += 'OR ANOTHER SNACK. '
133 choices += '99 TO READ AGAIN. '
134 choices += 'CHOICE? '
135 msg.append((choices, False, None))
140 info = pwd.getpwuid(uid)
142 logging.info('getting pin for uid %d: user not in password file'%uid)
144 if info.pw_dir == None: return False
145 pinfile = os.path.join(info.pw_dir, '.pin')
149 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
152 logging.info('getting pin for uid %d: .pin has wrong permissions. Fixing.'%uid)
153 os.chmod(pinfile, 0600)
157 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
159 pinstr = f.readline()
161 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
162 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
166 def has_good_pin(uid):
167 return get_pin(uid) != None
169 def verify_user_pin(uid, pin, skip_pin_check=False):
170 if skip_pin_check or get_pin(uid) == pin:
171 info = pwd.getpwuid(uid)
173 logging.info('accepted mifare for uid %d (%s)'%(uid,info.pw_name))
175 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
178 logging.info('refused pin for uid %d'%(uid))
184 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
185 choice = int(random()*len(messages))
186 msg = messages[choice]
187 left = range(len(msg))
188 for i in range(len(msg)):
189 if msg[i] == ' ': left.remove(i)
193 for i in range(0, len(msg)):
199 s += chr(int(random()*26)+ord('A'))
208 return ' '*((LEN-len(str))/2)+str
219 StringIdler(v, text="Kill 'em all", repeat=False),
220 GrayIdler(v,one="*",zero="-"),
221 StringIdler(v, text=CREDITS),
222 GrayIdler(v,one="/",zero="\\"),
224 GrayIdler(v,one="X",zero="O"),
225 FileIdler(v, '/usr/share/common-licenses/GPL-2',affinity=2),
226 GrayIdler(v,one="*",zero="-",reorder=1),
227 StringIdler(v, text=str(math.pi) + " "),
229 GrayIdler(v,one="/",zero="\\",reorder=1),
230 StringIdler(v, text=str(math.e) + " "),
231 GrayIdler(v,one="X",zero="O",reorder=1),
232 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),
233 PipeIdler(v, "/usr/bin/getent", "passwd"),
242 def reset_idler(v, vstatus, t = None):
244 idler = GreetingIdler(v, t)
245 vstatus.time_of_next_idlestep = time()+idler.next()
246 vstatus.time_of_next_idler = None
247 vstatus.time_to_autologout = None
248 vstatus.change_state(STATE_IDLE, 1)
253 average_affinity = 10 # guessing here...
255 if idler and idler.__class__ != GreetingIdler:
256 iiindex = idlers.index(idler)
260 move = int(random()*len(idlers)*average_affinity) + 1
265 idler = idlers[iiindex]
266 move -= idler.affinity()
270 def idle_step(vstatus):
274 vstatus.time_of_next_idler = time() + 30
275 nextidle = idler.next()
277 nextidle = IDLE_SPEED
278 vstatus.time_of_next_idlestep = time()+nextidle
281 def __init__(self,v):
282 self.state_table = {}
283 self.state = STATE_IDLE
286 self.mk = MessageKeeper(v)
290 self.cur_selection = ''
291 self.time_to_autologout = None
293 self.last_timeout_refresh = None
295 def change_state(self,newstate,newcounter=None):
296 if self.state != newstate:
297 #print "Changing state from: ",
301 self.state = newstate
303 if newcounter is not None and self.counter != newcounter:
304 #print "Changing counter from: ",
308 self.counter = newcounter
312 def handle_tick_event(event, params, v, vstatus):
313 # don't care right now.
316 def handle_switch_event(event, params, v, vstatus):
317 # don't care right now.
321 def do_nothing(state, event, params, v, vstatus):
322 print "doing nothing (s,e,p)", state, " ", event, " ", params
325 def handle_getting_uid_idle(state, event, params, v, vstatus):
326 # don't care right now.
329 def handle_getting_pin_idle(state, event, params, v, vstatus):
330 # don't care right now.
333 def handle_get_selection_idle(state, event, params, v, vstatus):
334 # don't care right now.
336 ### State logging out ..
337 if vstatus.time_to_autologout != None:
338 time_left = vstatus.time_to_autologout - time()
339 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
340 vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
341 vstatus.last_timeout_refresh = int(time_left)
342 vstatus.cur_selection = ''
344 if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
345 vstatus.time_to_autologout = None
346 vstatus.cur_user = ''
348 vstatus.cur_selection = ''
350 reset_idler(v, vstatus)
352 ### State fully logged out ... reset variables
353 if vstatus.time_to_autologout and not vstatus.mk.done():
354 vstatus.time_to_autologout = None
355 if vstatus.cur_user == '' and vstatus.time_to_autologout:
356 vstatus.time_to_autologout = None
359 if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
361 vstatus.time_to_autologout = time() + 15
362 vstatus.last_timeout_refresh = None
364 ## FIXME - this may need to be elsewhere.....
366 vstatus.mk.update_display()
370 def handle_get_selection_key(state, event, params, v, vstatus):
372 if len(vstatus.cur_selection) == 0:
375 vstatus.cur_user = ''
376 vstatus.cur_selection = ''
378 vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
379 reset_idler(v, vstatus, 2)
381 vstatus.cur_selection += chr(key + ord('0'))
382 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
383 vstatus.time_to_autologout = None
384 elif len(vstatus.cur_selection) == 1:
386 vstatus.cur_selection = ''
387 vstatus.time_to_autologout = None
388 scroll_options(vstatus.username, vstatus.mk)
391 vstatus.cur_selection += chr(key + ord('0'))
393 make_selection(v,vstatus)
394 vstatus.cur_selection = ''
395 vstatus.time_to_autologout = time() + 8
396 vstatus.last_timeout_refresh = None
399 price_check(v,vstatus)
400 vstatus.cur_selection = ''
401 vstatus.time_to_autologout = None
402 vstatus.last_timeout_refresh = None
404 def make_selection(v, vstatus):
405 # should use sudo here
406 if vstatus.cur_selection == '55':
407 vstatus.mk.set_message('OPENSESAME')
408 logging.info('dispensing a door for %s'%vstatus.username)
410 ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
412 ret = os.system('dispense door')
414 logging.info('door opened')
415 vstatus.mk.set_message(center('DOOR OPEN'))
417 logging.warning('user %s tried to dispense a bad door'%vstatus.username)
418 vstatus.mk.set_message(center('BAD DOOR'))
420 elif vstatus.cur_selection == '81':
422 elif vstatus.cur_selection == '99':
423 scroll_options(vstatus.username, vstatus.mk)
424 vstatus.cur_selection = ''
426 elif vstatus.cur_selection[1] == '8':
427 v.display('GOT DRINK?')
428 if ((os.system('su - "%s" -c "dispense %s"'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
429 v.display('SEEMS NOT')
431 v.display('GOT DRINK!')
433 # first see if it's a named slot
435 price, shortname, name = get_snack( vstatus.cur_selection )
437 price, shortname, name = get_snack( '--' )
438 dollarprice = "$%.2f" % ( price / 100.0 )
439 v.display(vstatus.cur_selection+' - %s'%dollarprice)
440 exitcode = os.system('su - "%s" -c "dispense give oday %d"'%(vstatus.username, price)) >> 8
442 # magic dispense syslog service
443 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
444 v.vend(vstatus.cur_selection)
445 v.display('THANK YOU')
447 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
448 v.display('NO MONEY?')
452 def price_check(v, vstatus):
453 if vstatus.cur_selection[1] == '8':
454 v.display(center('SEE COKE'))
456 # first see if it's a named slot
458 price, shortname, name = get_snack( vstatus.cur_selection )
460 price, shortname, name = get_snack( '--' )
461 dollarprice = "$%.2f" % ( price / 100.0 )
462 v.display(vstatus.cur_selection+' - %s'%dollarprice)
465 def handle_getting_pin_key(state, event, params, v, vstatus):
466 #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
468 if len(vstatus.cur_pin) < PIN_LENGTH:
470 if vstatus.cur_pin == '':
471 vstatus.cur_user = ''
472 reset_idler(v, vstatus)
476 vstatus.mk.set_message('PIN: ')
478 vstatus.cur_pin += chr(key + ord('0'))
479 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
480 if len(vstatus.cur_pin) == PIN_LENGTH:
481 vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
484 vstatus.cur_selection = ''
485 vstatus.change_state(STATE_GET_SELECTION)
486 scroll_options(vstatus.username, vstatus.mk, True)
490 vstatus.mk.set_messages(
491 [(center('BAD PIN'), False, 1.0),
492 (center('SORRY'), False, 0.5)])
493 vstatus.cur_user = ''
496 reset_idler(v, vstatus, 2)
501 def handle_getting_uid_key(state, event, params, v, vstatus):
502 #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
505 # complicated key handling here:
507 if len(vstatus.cur_user) == 0 and key == 9:
508 vstatus.cur_selection = ''
509 vstatus.time_to_autologout = None
510 vstatus.mk.set_message('PRICECHECK')
512 scroll_options('', vstatus.mk)
513 vstatus.change_state(STATE_GET_SELECTION)
516 if len(vstatus.cur_user) <8:
518 vstatus.cur_user = ''
520 reset_idler(v, vstatus)
522 vstatus.cur_user += chr(key + ord('0'))
523 #logging.info('dob: '+vstatus.cur_user)
524 if len(vstatus.cur_user) > 5:
525 vstatus.mk.set_message('>'+vstatus.cur_user)
527 vstatus.mk.set_message('UID: '+vstatus.cur_user)
529 if len(vstatus.cur_user) == 5:
530 uid = int(vstatus.cur_user)
533 logging.info('user '+vstatus.cur_user+' has a bad PIN')
539 Welcome to Picklevision Sytems, Sunnyvale, CA
541 Greetings Professor Falken.
546 Shall we play a game?
549 Please choose from the following menu:
556 6. Toxic and Biochemical Warfare
557 7. Global Thermonuclear War
561 Wouldn't you prefer a nice game of chess?
563 """.replace('\n',' ')
564 vstatus.mk.set_messages([(pfalken, False, 10)])
565 vstatus.cur_user = ''
568 reset_idler(v, vstatus, 10)
572 if not has_good_pin(uid):
573 logging.info('user '+vstatus.cur_user+' has a bad PIN')
574 vstatus.mk.set_messages(
575 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
576 vstatus.cur_user = ''
579 reset_idler(v, vstatus, 3)
585 vstatus.mk.set_message('PIN: ')
586 logging.info('need pin for user %s'%vstatus.cur_user)
587 vstatus.change_state(STATE_GETTING_PIN)
591 def handle_idle_key(state, event, params, v, vstatus):
592 #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
597 vstatus.cur_user = ''
598 reset_idler(v, vstatus)
601 vstatus.change_state(STATE_GETTING_UID)
602 run_handler(event, key, v, vstatus)
605 def handle_idle_tick(state, event, params, v, vstatus):
607 if vstatus.mk.done():
610 if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
611 vstatus.time_of_next_idler = time() + 30
616 vstatus.mk.update_display()
618 vstatus.change_state(STATE_GRANDFATHER_CLOCK)
619 run_handler(event, params, v, vstatus)
622 def beep_on(when, before=0):
623 start = int(when - before)
627 if now >= start and now <= end:
631 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
632 ### check for interesting times
635 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
636 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
637 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
638 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
640 hourfromnow = localtime(time() + 3600)
642 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
643 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
644 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
646 ## check for X seconds to the hour
647 ## if case, update counter to 2
648 if beep_on(onthehour,15) \
649 or beep_on(halfhour,0) \
650 or beep_on(quarterhour,0) \
651 or beep_on(threequarterhour,0) \
652 or beep_on(fivetothehour,0):
653 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
654 run_handler(event, params, v, vstatus)
656 vstatus.change_state(STATE_IDLE)
658 def handle_grandfather_tick(state, event, params, v, vstatus):
662 ### we live in interesting times
665 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
666 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
667 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
668 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
670 hourfromnow = localtime(time() + 3600)
672 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
673 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
674 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
677 #print "when it fashionable to wear a onion on your hip"
679 if beep_on(onthehour,15):
681 next_hour=((hourfromnow[3] + 11) % 12) + 1
682 if onthehour - time() < next_hour and onthehour - time() > 0:
687 msg.append(("DING!", False, None))
689 msg.append((" DING!", False, None))
690 elif int(onthehour - time()) == 0:
692 msg.append((" BONG!", False, None))
693 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
694 elif beep_on(halfhour,0):
697 msg.append((" HALFHOUR ", False, 50))
698 elif beep_on(quarterhour,0):
701 msg.append((" QTR HOUR ", False, 50))
702 elif beep_on(threequarterhour,0):
705 msg.append((" 3 QTR HR ", False, 50))
706 elif beep_on(fivetothehour,0):
709 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
713 ## check for X seconds to the hour
716 vstatus.mk.set_messages(msg)
719 vstatus.mk.update_display()
720 ## if no longer case, return to idle
722 ## change idler to be clock
723 if go_idle and vstatus.mk.done():
724 vstatus.change_state(STATE_IDLE,1)
726 def handle_door_idle(state, event, params, v, vstatus):
727 def twiddle(clock,v,wise = 2):
729 v.display("-FEED ME-")
730 elif (clock % 4 == 1+wise):
731 v.display("\\FEED ME/")
732 elif (clock % 4 == 2):
733 v.display("-FEED ME-")
734 elif (clock % 4 == 3-wise):
735 v.display("/FEED ME\\")
737 # don't care right now.
740 if ((now % 60 % 2) == 0):
743 twiddle(now, v, wise=0)
746 def handle_door_event(state, event, params, v, vstatus):
747 if params == 0: #door open
748 vstatus.change_state(STATE_DOOR_OPENING)
749 logging.warning("Entering open door mode")
750 v.display("-FEED ME-")
752 vstatus.cur_user = ''
754 elif params == 1: #door closed
755 vstatus.change_state(STATE_DOOR_CLOSING)
756 reset_idler(v, vstatus, 3)
758 logging.warning('Leaving open door mode')
759 v.display("-YUM YUM!-")
761 def handle_mifare_event(state, event, params, v, vstatus):
763 # Translate card_id into uid.
768 vstatus.cur_user = get_uid(card_id)
769 logging.info('Mapped card id to uid %s'%vstatus.cur_user)
770 vstatus.username = verify_user_pin(int(vstatus.cur_user), None, True)
772 vstatus.username = None
775 vstatus.cur_selection = ''
776 vstatus.change_state(STATE_GET_SELECTION)
777 scroll_options(vstatus.username, vstatus.mk, True)
781 vstatus.mk.set_messages(
782 [(center('BAD CARD'), False, 1.0),
783 (center('SORRY'), False, 0.5)])
784 vstatus.cur_user = ''
787 reset_idler(v, vstatus, 2)
790 def handle_mifare_add_user_event(state, event, params, v, vstatus):
793 # Translate card_id into uid.
798 if get_uid(card_id) != None:
799 vstatus.mk.set_messages(
800 [(center('ALREADY'), False, 0.5),
801 (center('ENROLLED'), False, 0.5)])
803 # scroll_options(vstatus.username, vstatus.mk)
808 logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
809 set_card_id(vstatus.cur_user, card_id)
810 vstatus.mk.set_messages(
811 [(center('CARD'), False, 0.5),
812 (center('ENROLLED'), False, 0.5)])
814 # scroll_options(vstatus.username, vstatus.mk)
816 def return_to_idle(state,event,params,v,vstatus):
817 reset_idler(v, vstatus)
819 def create_state_table(vstatus):
820 vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
821 vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
822 vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
823 vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
825 vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
826 vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
827 vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
828 vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
830 vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
831 vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
832 vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
833 vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
835 vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
836 vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
837 vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
838 vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
840 vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
841 vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
842 vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
843 vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
845 vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
846 vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
847 vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
848 vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
850 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
851 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
852 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
853 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
854 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
855 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
856 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
858 def get_state_table_handler(vstatus, state, event, counter):
859 return vstatus.state_table[(state,event,counter)]
861 def time_to_next_update(vstatus):
862 idle_update = vstatus.time_of_next_idlestep - time()
863 if not vstatus.mk.done() and vstatus.mk.next_update is not None:
864 mk_update = vstatus.mk.next_update - time()
865 if mk_update < idle_update:
866 idle_update = mk_update
869 def run_forever(rfh, wfh, options, cf):
870 v = VendingMachine(rfh, wfh, USE_MIFARE)
871 vstatus = VendState(v)
872 create_state_table(vstatus)
874 logging.debug('PING is ' + str(v.ping()))
876 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
879 reset_idler(v, vstatus)
881 # This main loop was hideous and the work of the devil.
882 # This has now been fixed (mostly) - mtearle
885 # notes for later surgery
886 # (event, counter, ' ')
890 # ( return state - not currently implemented )
896 except DispenseDatabaseException, e:
897 logging.error('Database error: '+str(e))
899 timeout = time_to_next_update(vstatus)
900 e = v.next_event(timeout)
903 run_handler(event, params, v, vstatus)
905 # logging.debug('Got event: ' + repr(e))
908 def run_handler(event, params, v, vstatus):
909 handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
911 handler(vstatus.state, event, params, v, vstatus)
913 def connect_to_vend(options, cf):
916 logging.info('Connecting to vending machine using LAT')
917 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
918 rfh, wfh = latclient.get_fh()
919 elif options.use_serial:
920 # Open vending machine via serial.
921 logging.info('Connecting to vending machine using serial')
922 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
923 rfh,wfh = serialclient.get_fh()
925 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
926 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
928 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
929 sock.connect((options.host, options.port))
930 rfh = sock.makefile('r')
931 wfh = sock.makefile('w')
938 from optparse import OptionParser
940 op = OptionParser(usage="%prog [OPTION]...")
941 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')
942 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
943 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
944 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
945 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
946 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
947 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
948 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
949 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
950 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
951 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
952 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
953 options, args = op.parse_args()
956 op.error('extra command line arguments: ' + ' '.join(args))
961 'DBServer': ('Database', 'Server'),
962 'DBName': ('Database', 'Name'),
963 'DBUser': ('VendingMachine', 'DBUser'),
964 'DBPassword': ('VendingMachine', 'DBPassword'),
966 'ServiceName': ('VendingMachine', 'ServiceName'),
967 'ServicePassword': ('VendingMachine', 'Password'),
969 'ServerName': ('DecServer', 'Name'),
970 'ConnectPassword': ('DecServer', 'ConnectPassword'),
971 'PrivPassword': ('DecServer', 'PrivPassword'),
974 class VendConfigFile:
975 def __init__(self, config_file, options):
977 cp = ConfigParser.ConfigParser()
980 for option in options:
981 section, name = options[option]
982 value = cp.get(section, name)
983 self.__dict__[option] = value
985 except ConfigParser.Error, e:
986 raise SystemExit("Error reading config file "+config_file+": " + str(e))
988 def create_pid_file(name):
990 pid_file = file(name, 'w')
991 pid_file.write('%d\n'%os.getpid())
994 logging.warning('unable to write to pid file '+name+': '+str(e))
997 def do_nothing(signum, stack):
998 signal.signal(signum, do_nothing)
999 def stop_server(signum, stack): raise KeyboardInterrupt
1000 signal.signal(signal.SIGHUP, do_nothing)
1001 signal.signal(signal.SIGTERM, stop_server)
1002 signal.signal(signal.SIGINT, stop_server)
1004 options = parse_args()
1005 config_opts = VendConfigFile(options.config_file, config_options)
1006 if options.daemon: become_daemon()
1007 set_up_logging(options)
1008 if options.pid_file != '': create_pid_file(options.pid_file)
1010 return options, config_opts
1012 def clean_up_nicely(options, config_opts):
1013 if options.pid_file != '':
1015 os.unlink(options.pid_file)
1016 logging.debug('Removed pid file '+options.pid_file)
1017 except OSError: pass # if we can't delete it, meh
1019 def set_up_logging(options):
1020 logger = logging.getLogger()
1022 if not options.daemon:
1023 stderr_logger = logging.StreamHandler(sys.stderr)
1024 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1025 logger.addHandler(stderr_logger)
1027 if options.log_file != '':
1029 file_logger = logging.FileHandler(options.log_file)
1030 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1031 logger.addHandler(file_logger)
1033 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1035 if options.syslog != None:
1036 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1037 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1038 logger.addHandler(sys_logger)
1041 logger.setLevel(logging.WARNING)
1042 elif options.verbose:
1043 logger.setLevel(logging.DEBUG)
1045 logger.setLevel(logging.INFO)
1047 def become_daemon():
1048 dev_null = file('/dev/null')
1049 fd = dev_null.fileno()
1058 raise SystemExit('failed to fork: '+str(e))
1060 def do_vend_server(options, config_opts):
1063 rfh, wfh = connect_to_vend(options, config_opts)
1064 except (SerialClientException, socket.error), e:
1065 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1067 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1068 logging.info("Trying again in 5 seconds.")
1073 run_forever(rfh, wfh, options, config_opts)
1074 except VendingException:
1075 logging.error("Connection died, trying again...")
1076 logging.info("Trying again in 5 seconds.")
1079 if __name__ == '__main__':
1080 options, config_opts = set_stuff_up()
1083 logging.warning('Starting Vend Server')
1084 do_vend_server(options, config_opts)
1085 logging.error('Vend Server finished unexpectedly, restarting')
1086 except KeyboardInterrupt:
1087 logging.info("Killed by signal, cleaning up")
1088 clean_up_nicely(options, config_opts)
1089 logging.warning("Vend Server stopped")
1094 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1095 tb = format_tb(exc_traceback, 20)
1098 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1099 logging.critical("Message: " + str(exc_value))
1100 logging.critical("Traceback:")
1102 for line in event.split('\n'):
1103 logging.critical(' '+line)
1104 logging.critical("This message should be considered a bug in the Vend Server.")
1105 logging.critical("Please report this to someone who can fix it.")
1107 logging.warning("Trying again anyway (might not help, but hey...)")