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_snack#, get_snacks
23 from posix import geteuid
24 from LDAPConnector import get_uid, set_card_id
27 This vending machine software brought to you by:
32 and a collective of hungry alpacas.
36 For a good time call +61 8 6488 3901
58 STATE_GRANDFATHER_CLOCK,
64 class DispenseDatabaseException(Exception): pass
66 class DispenseDatabase:
67 def __init__(self, vending_machine, host, name, user, password):
68 self.vending_machine = vending_machine
69 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
70 self.db.query('LISTEN vend_requests')
72 def process_requests(self):
73 logging.debug('database processing')
74 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
76 outstanding = self.db.query(query).getresult()
77 except (pg.error,), db_err:
78 raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
79 for (id, slot) in outstanding:
80 (worked, code, string) = self.vending_machine.vend(slot)
81 logging.debug (str((worked, code, string)))
83 query = 'SELECT vend_success(%s)'%id
84 self.db.query(query).getresult()
86 query = 'SELECT vend_failed(%s)'%id
87 self.db.query(query).getresult()
89 def handle_events(self):
90 notifier = self.db.getnotify()
91 while notifier is not None:
92 self.process_requests()
93 notify = self.db.getnotify()
95 def scroll_options(username, mk, welcome = False):
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: '
114 for i in range(0, 7):
115 cmd = 'dispense iteminfo coke:%i' % i
119 m = re.match("\s*[a-z]+:\d+\s+(\d+)\.(\d\d)\s+([^\n]+)", info)
120 cents = int(m.group(1))*100 + int(m.group(2))
121 cokes.append('%i %i %s' % (i, cents, m.group(3)));
125 (slot_num, price, slot_name) = c.split(' ', 2)
126 if slot_name == 'dead': continue
127 choices += '%s-(%sc)-%s8 '%(slot_name, price, slot_num)
129 # we don't want to print snacks for now since it'll be too large
130 # and there's physical bits of paper in the machine anyway - matt
132 # snacks = get_snacks()
136 # for slot, ( name, price ) in snacks.items():
137 # choices += '%s8-%s (%sc) ' % ( slot, name, price )
139 choices += '55-DOOR '
140 choices += 'OR ANOTHER SNACK. '
141 choices += '99 TO READ AGAIN. '
142 choices += 'CHOICE? '
143 msg.append((choices, False, None))
148 info = pwd.getpwuid(uid)
150 logging.info('getting pin for uid %d: user not in password file'%uid)
152 if info.pw_dir == None: return False
153 pinfile = os.path.join(info.pw_dir, '.pin')
157 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
160 logging.info('getting pin for uid %d: .pin has wrong permissions. Fixing.'%uid)
161 os.chmod(pinfile, 0600)
165 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
167 pinstr = f.readline()
169 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
170 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
174 def has_good_pin(uid):
175 return get_pin(uid) != None
177 def verify_user_pin(uid, pin, skip_pin_check=False):
178 if skip_pin_check or get_pin(uid) == pin:
179 info = pwd.getpwuid(uid)
181 logging.info('accepted mifare for uid %d (%s)'%(uid,info.pw_name))
183 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
186 logging.info('refused pin for uid %d'%(uid))
192 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
193 choice = int(random()*len(messages))
194 msg = messages[choice]
195 left = range(len(msg))
196 for i in range(len(msg)):
197 if msg[i] == ' ': left.remove(i)
201 for i in range(0, len(msg)):
207 s += chr(int(random()*26)+ord('A'))
216 return ' '*((LEN-len(str))/2)+str
227 StringIdler(v, text="Kill 'em all", repeat=False),
228 GrayIdler(v,one="*",zero="-"),
229 StringIdler(v, text=CREDITS),
230 GrayIdler(v,one="/",zero="\\"),
232 GrayIdler(v,one="X",zero="O"),
233 FileIdler(v, '/usr/share/common-licenses/GPL-2',affinity=2),
234 GrayIdler(v,one="*",zero="-",reorder=1),
235 StringIdler(v, text=str(math.pi) + " "),
237 GrayIdler(v,one="/",zero="\\",reorder=1),
238 StringIdler(v, text=str(math.e) + " "),
239 GrayIdler(v,one="X",zero="O",reorder=1),
240 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),
241 PipeIdler(v, "/usr/bin/getent", "passwd"),
250 def reset_idler(v, vstatus, t = None):
252 idler = GreetingIdler(v, t)
253 vstatus.time_of_next_idlestep = time()+idler.next()
254 vstatus.time_of_next_idler = None
255 vstatus.time_to_autologout = None
256 vstatus.change_state(STATE_IDLE, 1)
261 average_affinity = 10 # guessing here...
263 if idler and idler.__class__ != GreetingIdler:
264 iiindex = idlers.index(idler)
268 move = int(random()*len(idlers)*average_affinity) + 1
273 idler = idlers[iiindex]
274 move -= idler.affinity()
278 def idle_step(vstatus):
282 vstatus.time_of_next_idler = time() + 30
283 nextidle = idler.next()
285 nextidle = IDLE_SPEED
286 vstatus.time_of_next_idlestep = time()+nextidle
289 def __init__(self,v):
290 self.state_table = {}
291 self.state = STATE_IDLE
294 self.mk = MessageKeeper(v)
298 self.cur_selection = ''
299 self.time_to_autologout = None
301 self.last_timeout_refresh = None
303 def change_state(self,newstate,newcounter=None):
304 if self.state != newstate:
305 #print "Changing state from: ",
309 self.state = newstate
311 if newcounter is not None and self.counter != newcounter:
312 #print "Changing counter from: ",
316 self.counter = newcounter
320 def handle_tick_event(event, params, v, vstatus):
321 # don't care right now.
324 def handle_switch_event(event, params, v, vstatus):
325 # don't care right now.
329 def do_nothing(state, event, params, v, vstatus):
330 print "doing nothing (s,e,p)", state, " ", event, " ", params
333 def handle_getting_uid_idle(state, event, params, v, vstatus):
334 # don't care right now.
337 def handle_getting_pin_idle(state, event, params, v, vstatus):
338 # don't care right now.
341 def handle_get_selection_idle(state, event, params, v, vstatus):
342 # don't care right now.
344 ### State logging out ..
345 if vstatus.time_to_autologout != None:
346 time_left = vstatus.time_to_autologout - time()
347 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
348 vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
349 vstatus.last_timeout_refresh = int(time_left)
350 vstatus.cur_selection = ''
352 if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
353 vstatus.time_to_autologout = None
354 vstatus.cur_user = ''
356 vstatus.cur_selection = ''
358 reset_idler(v, vstatus)
360 ### State fully logged out ... reset variables
361 if vstatus.time_to_autologout and not vstatus.mk.done():
362 vstatus.time_to_autologout = None
363 if vstatus.cur_user == '' and vstatus.time_to_autologout:
364 vstatus.time_to_autologout = None
367 if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
369 vstatus.time_to_autologout = time() + 15
370 vstatus.last_timeout_refresh = None
372 ## FIXME - this may need to be elsewhere.....
374 vstatus.mk.update_display()
378 def handle_get_selection_key(state, event, params, v, vstatus):
380 if len(vstatus.cur_selection) == 0:
383 vstatus.cur_user = ''
384 vstatus.cur_selection = ''
386 vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
387 reset_idler(v, vstatus, 2)
389 vstatus.cur_selection += chr(key + ord('0'))
390 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
391 vstatus.time_to_autologout = None
392 elif len(vstatus.cur_selection) == 1:
394 vstatus.cur_selection = ''
395 vstatus.time_to_autologout = None
396 scroll_options(vstatus.username, vstatus.mk)
399 vstatus.cur_selection += chr(key + ord('0'))
401 make_selection(v,vstatus)
402 vstatus.cur_selection = ''
403 vstatus.time_to_autologout = time() + 8
404 vstatus.last_timeout_refresh = None
407 price_check(v,vstatus)
408 vstatus.cur_selection = ''
409 vstatus.time_to_autologout = None
410 vstatus.last_timeout_refresh = None
412 def make_selection(v, vstatus):
413 # should use sudo here
414 if vstatus.cur_selection == '55':
415 vstatus.mk.set_message('OPENSESAME')
416 logging.info('dispensing a door for %s'%vstatus.username)
418 #ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
419 ret = os.system('dispense -u "%s" door'%vstatus.username)
421 ret = os.system('dispense door')
423 logging.info('door opened')
424 vstatus.mk.set_message(center('DOOR OPEN'))
426 logging.warning('user %s tried to dispense a bad door'%vstatus.username)
427 vstatus.mk.set_message(center('BAD DOOR'))
429 elif vstatus.cur_selection == '81':
431 elif vstatus.cur_selection == '99':
432 scroll_options(vstatus.username, vstatus.mk)
433 vstatus.cur_selection = ''
435 elif vstatus.cur_selection[1] == '8':
436 v.display('GOT DRINK?')
437 if ((os.system('dispense -u "%s" coke:%s'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
438 v.display('SEEMS NOT')
440 v.display('GOT DRINK!')
442 # first see if it's a named slot
444 price, shortname, name = get_snack( vstatus.cur_selection )
446 price, shortname, name = get_snack( '--' )
447 dollarprice = "$%.2f" % ( price / 100.0 )
448 v.display(vstatus.cur_selection+' - %s'%dollarprice)
449 exitcode = os.system('dispense -u "%s" give \>snacksales %d "%s"'%(vstatus.username, price, name)) >> 8
450 # exitcode = os.system('dispense -u "%s" give \>sales\:snack %d "%s"'%(vstatus.username, price, name)) >> 8
451 # exitcode = os.system('dispense -u "%s" snack:%s'%(vstatus.username, vstatus.cur_selection)) >> 8
453 # magic dispense syslog service
454 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
455 v.vend(vstatus.cur_selection)
456 v.display('THANK YOU')
457 elif (exitcode == 5): # RV_BALANCE
458 v.display('NO MONEY?')
459 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
460 v.display('EMPTY SLOT')
461 elif (exitcode == 1): # RV_BADITEM (Dead slot)
462 v.display('EMPTY SLOT')
464 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
465 v.display('UNK ERROR')
469 def price_check(v, vstatus):
470 if vstatus.cur_selection[1] == '8':
471 v.display(center('SEE COKE'))
473 # first see if it's a named slot
475 price, shortname, name = get_snack( vstatus.cur_selection )
477 price, shortname, name = get_snack( '--' )
478 dollarprice = "$%.2f" % ( price / 100.0 )
479 v.display(vstatus.cur_selection+' - %s'%dollarprice)
482 def handle_getting_pin_key(state, event, params, v, vstatus):
483 #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
485 if len(vstatus.cur_pin) < PIN_LENGTH:
487 if vstatus.cur_pin == '':
488 vstatus.cur_user = ''
489 reset_idler(v, vstatus)
493 vstatus.mk.set_message('PIN: ')
495 vstatus.cur_pin += chr(key + ord('0'))
496 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
497 if len(vstatus.cur_pin) == PIN_LENGTH:
498 vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
501 vstatus.cur_selection = ''
502 vstatus.change_state(STATE_GET_SELECTION)
503 scroll_options(vstatus.username, vstatus.mk, True)
507 vstatus.mk.set_messages(
508 [(center('BAD PIN'), False, 1.0),
509 (center('SORRY'), False, 0.5)])
510 vstatus.cur_user = ''
513 reset_idler(v, vstatus, 2)
518 def handle_getting_uid_key(state, event, params, v, vstatus):
519 #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
522 # complicated key handling here:
524 if len(vstatus.cur_user) == 0 and key == 9:
525 vstatus.cur_selection = ''
526 vstatus.time_to_autologout = None
527 vstatus.mk.set_message('PRICECHECK')
529 scroll_options('', vstatus.mk)
530 vstatus.change_state(STATE_GET_SELECTION)
533 if len(vstatus.cur_user) <8:
535 vstatus.cur_user = ''
537 reset_idler(v, vstatus)
539 vstatus.cur_user += chr(key + ord('0'))
540 #logging.info('dob: '+vstatus.cur_user)
541 if len(vstatus.cur_user) > 5:
542 vstatus.mk.set_message('>'+vstatus.cur_user)
544 vstatus.mk.set_message('UID: '+vstatus.cur_user)
546 if len(vstatus.cur_user) == 5:
547 uid = int(vstatus.cur_user)
550 logging.info('user '+vstatus.cur_user+' has a bad PIN')
556 Welcome to Picklevision Sytems, Sunnyvale, CA
558 Greetings Professor Falken.
563 Shall we play a game?
566 Please choose from the following menu:
573 6. Toxic and Biochemical Warfare
574 7. Global Thermonuclear War
578 Wouldn't you prefer a nice game of chess?
580 """.replace('\n',' ')
581 vstatus.mk.set_messages([(pfalken, False, 10)])
582 vstatus.cur_user = ''
585 reset_idler(v, vstatus, 10)
589 if not has_good_pin(uid):
590 logging.info('user '+vstatus.cur_user+' has a bad PIN')
591 vstatus.mk.set_messages(
592 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
593 vstatus.cur_user = ''
596 reset_idler(v, vstatus, 3)
602 vstatus.mk.set_message('PIN: ')
603 logging.info('need pin for user %s'%vstatus.cur_user)
604 vstatus.change_state(STATE_GETTING_PIN)
608 def handle_idle_key(state, event, params, v, vstatus):
609 #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
614 vstatus.cur_user = ''
615 reset_idler(v, vstatus)
618 vstatus.change_state(STATE_GETTING_UID)
619 run_handler(event, key, v, vstatus)
622 def handle_idle_tick(state, event, params, v, vstatus):
624 if vstatus.mk.done():
627 if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
628 vstatus.time_of_next_idler = time() + 30
633 vstatus.mk.update_display()
635 vstatus.change_state(STATE_GRANDFATHER_CLOCK)
636 run_handler(event, params, v, vstatus)
639 def beep_on(when, before=0):
640 start = int(when - before)
644 if now >= start and now <= end:
648 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
649 ### check for interesting times
652 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
653 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
654 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
655 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
657 hourfromnow = localtime(time() + 3600)
659 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
660 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
661 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
663 ## check for X seconds to the hour
664 ## if case, update counter to 2
665 if beep_on(onthehour,15) \
666 or beep_on(halfhour,0) \
667 or beep_on(quarterhour,0) \
668 or beep_on(threequarterhour,0) \
669 or beep_on(fivetothehour,0):
670 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
671 run_handler(event, params, v, vstatus)
673 vstatus.change_state(STATE_IDLE)
675 def handle_grandfather_tick(state, event, params, v, vstatus):
679 ### we live in interesting times
682 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
683 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
684 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
685 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
687 hourfromnow = localtime(time() + 3600)
689 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
690 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
691 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
694 #print "when it fashionable to wear a onion on your hip"
696 if beep_on(onthehour,15):
698 next_hour=((hourfromnow[3] + 11) % 12) + 1
699 if onthehour - time() < next_hour and onthehour - time() > 0:
704 msg.append(("DING!", False, None))
706 msg.append((" DING!", False, None))
707 elif int(onthehour - time()) == 0:
709 msg.append((" BONG!", False, None))
710 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
711 elif beep_on(halfhour,0):
714 msg.append((" HALFHOUR ", False, 50))
715 elif beep_on(quarterhour,0):
718 msg.append((" QTR HOUR ", False, 50))
719 elif beep_on(threequarterhour,0):
722 msg.append((" 3 QTR HR ", False, 50))
723 elif beep_on(fivetothehour,0):
726 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
730 ## check for X seconds to the hour
733 vstatus.mk.set_messages(msg)
736 vstatus.mk.update_display()
737 ## if no longer case, return to idle
739 ## change idler to be clock
740 if go_idle and vstatus.mk.done():
741 vstatus.change_state(STATE_IDLE,1)
743 def handle_door_idle(state, event, params, v, vstatus):
744 def twiddle(clock,v,wise = 2):
746 v.display("-FEED ME-")
747 elif (clock % 4 == 1+wise):
748 v.display("\\FEED ME/")
749 elif (clock % 4 == 2):
750 v.display("-FEED ME-")
751 elif (clock % 4 == 3-wise):
752 v.display("/FEED ME\\")
754 # don't care right now.
757 if ((now % 60 % 2) == 0):
760 twiddle(now, v, wise=0)
763 def handle_door_event(state, event, params, v, vstatus):
764 if params == 0: #door open
765 vstatus.change_state(STATE_DOOR_OPENING)
766 logging.warning("Entering open door mode")
767 v.display("-FEED ME-")
769 vstatus.cur_user = ''
771 elif params == 1: #door closed
772 vstatus.change_state(STATE_DOOR_CLOSING)
773 reset_idler(v, vstatus, 3)
775 logging.warning('Leaving open door mode')
776 v.display("-YUM YUM!-")
778 def handle_mifare_event(state, event, params, v, vstatus):
780 # Translate card_id into uid.
785 vstatus.cur_user = get_uid(card_id)
786 logging.info('Mapped card id to uid %s'%vstatus.cur_user)
787 vstatus.username = verify_user_pin(int(vstatus.cur_user), None, True)
789 vstatus.username = None
792 vstatus.cur_selection = ''
793 vstatus.change_state(STATE_GET_SELECTION)
794 scroll_options(vstatus.username, vstatus.mk, True)
798 vstatus.mk.set_messages(
799 [(center('BAD CARD'), False, 1.0),
800 (center('SORRY'), False, 0.5)])
801 vstatus.cur_user = ''
804 reset_idler(v, vstatus, 2)
807 def handle_mifare_add_user_event(state, event, params, v, vstatus):
810 # Translate card_id into uid.
815 if get_uid(card_id) != None:
816 vstatus.mk.set_messages(
817 [(center('ALREADY'), False, 0.5),
818 (center('ENROLLED'), False, 0.5)])
820 # scroll_options(vstatus.username, vstatus.mk)
825 logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
826 set_card_id(vstatus.cur_user, card_id)
827 vstatus.mk.set_messages(
828 [(center('CARD'), False, 0.5),
829 (center('ENROLLED'), False, 0.5)])
831 # scroll_options(vstatus.username, vstatus.mk)
833 def return_to_idle(state,event,params,v,vstatus):
834 reset_idler(v, vstatus)
836 def create_state_table(vstatus):
837 vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
838 vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
839 vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
840 vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
842 vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
843 vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
844 vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
845 vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
847 vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
848 vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
849 vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
850 vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
852 vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
853 vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
854 vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
855 vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
857 vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
858 vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
859 vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
860 vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
862 vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
863 vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
864 vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
865 vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
867 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
868 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
869 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
870 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
871 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
872 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
873 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
875 def get_state_table_handler(vstatus, state, event, counter):
876 return vstatus.state_table[(state,event,counter)]
878 def time_to_next_update(vstatus):
879 idle_update = vstatus.time_of_next_idlestep - time()
880 if not vstatus.mk.done() and vstatus.mk.next_update is not None:
881 mk_update = vstatus.mk.next_update - time()
882 if mk_update < idle_update:
883 idle_update = mk_update
886 def run_forever(rfh, wfh, options, cf):
887 v = VendingMachine(rfh, wfh, USE_MIFARE)
888 vstatus = VendState(v)
889 create_state_table(vstatus)
891 logging.debug('PING is ' + str(v.ping()))
893 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
896 reset_idler(v, vstatus)
898 # This main loop was hideous and the work of the devil.
899 # This has now been fixed (mostly) - mtearle
902 # notes for later surgery
903 # (event, counter, ' ')
907 # ( return state - not currently implemented )
913 except DispenseDatabaseException, e:
914 logging.error('Database error: '+str(e))
916 timeout = time_to_next_update(vstatus)
917 e = v.next_event(timeout)
920 run_handler(event, params, v, vstatus)
922 # logging.debug('Got event: ' + repr(e))
925 def run_handler(event, params, v, vstatus):
926 handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
928 handler(vstatus.state, event, params, v, vstatus)
930 def connect_to_vend(options, cf):
933 logging.info('Connecting to vending machine using LAT')
934 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
935 rfh, wfh = latclient.get_fh()
936 elif options.use_serial:
937 # Open vending machine via serial.
938 logging.info('Connecting to vending machine using serial')
939 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
940 rfh,wfh = serialclient.get_fh()
942 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
943 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
945 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
946 sock.connect((options.host, options.port))
947 rfh = sock.makefile('r')
948 wfh = sock.makefile('w')
955 from optparse import OptionParser
957 op = OptionParser(usage="%prog [OPTION]...")
958 op.add_option('-f', '--config-file', default='/etc/dispense2/servers.conf', metavar='FILE', dest='config_file', help='use the specified config file instead of /etc/dispense/servers.conf')
959 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
960 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
961 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
962 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
963 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
964 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
965 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
966 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
967 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
968 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
969 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
970 options, args = op.parse_args()
973 op.error('extra command line arguments: ' + ' '.join(args))
978 'DBServer': ('Database', 'Server'),
979 'DBName': ('Database', 'Name'),
980 'DBUser': ('VendingMachine', 'DBUser'),
981 'DBPassword': ('VendingMachine', 'DBPassword'),
983 'ServiceName': ('VendingMachine', 'ServiceName'),
984 'ServicePassword': ('VendingMachine', 'Password'),
986 'ServerName': ('DecServer', 'Name'),
987 'ConnectPassword': ('DecServer', 'ConnectPassword'),
988 'PrivPassword': ('DecServer', 'PrivPassword'),
991 class VendConfigFile:
992 def __init__(self, config_file, options):
994 cp = ConfigParser.ConfigParser()
997 for option in options:
998 section, name = options[option]
999 value = cp.get(section, name)
1000 self.__dict__[option] = value
1002 except ConfigParser.Error, e:
1003 raise SystemExit("Error reading config file "+config_file+": " + str(e))
1005 def create_pid_file(name):
1007 pid_file = file(name, 'w')
1008 pid_file.write('%d\n'%os.getpid())
1011 logging.warning('unable to write to pid file '+name+': '+str(e))
1014 def do_nothing(signum, stack):
1015 signal.signal(signum, do_nothing)
1016 def stop_server(signum, stack): raise KeyboardInterrupt
1017 signal.signal(signal.SIGHUP, do_nothing)
1018 signal.signal(signal.SIGTERM, stop_server)
1019 signal.signal(signal.SIGINT, stop_server)
1021 options = parse_args()
1022 config_opts = VendConfigFile(options.config_file, config_options)
1023 if options.daemon: become_daemon()
1024 set_up_logging(options)
1025 if options.pid_file != '': create_pid_file(options.pid_file)
1027 return options, config_opts
1029 def clean_up_nicely(options, config_opts):
1030 if options.pid_file != '':
1032 os.unlink(options.pid_file)
1033 logging.debug('Removed pid file '+options.pid_file)
1034 except OSError: pass # if we can't delete it, meh
1036 def set_up_logging(options):
1037 logger = logging.getLogger()
1039 if not options.daemon:
1040 stderr_logger = logging.StreamHandler(sys.stderr)
1041 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1042 logger.addHandler(stderr_logger)
1044 if options.log_file != '':
1046 file_logger = logging.FileHandler(options.log_file)
1047 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1048 logger.addHandler(file_logger)
1050 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1052 if options.syslog != None:
1053 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1054 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1055 logger.addHandler(sys_logger)
1058 logger.setLevel(logging.WARNING)
1059 elif options.verbose:
1060 logger.setLevel(logging.DEBUG)
1062 logger.setLevel(logging.INFO)
1064 def become_daemon():
1065 dev_null = file('/dev/null')
1066 fd = dev_null.fileno()
1075 raise SystemExit('failed to fork: '+str(e))
1077 def do_vend_server(options, config_opts):
1080 rfh, wfh = connect_to_vend(options, config_opts)
1081 except (SerialClientException, socket.error), e:
1082 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1084 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1085 logging.info("Trying again in 5 seconds.")
1089 # run_forever(rfh, wfh, options, config_opts)
1092 run_forever(rfh, wfh, options, config_opts)
1093 except VendingException:
1094 logging.error("Connection died, trying again...")
1095 logging.info("Trying again in 5 seconds.")
1098 if __name__ == '__main__':
1099 options, config_opts = set_stuff_up()
1102 logging.warning('Starting Vend Server')
1103 do_vend_server(options, config_opts)
1104 logging.error('Vend Server finished unexpectedly, restarting')
1105 except KeyboardInterrupt:
1106 logging.info("Killed by signal, cleaning up")
1107 clean_up_nicely(options, config_opts)
1108 logging.warning("Vend Server stopped")
1113 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1114 tb = format_tb(exc_traceback, 20)
1117 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1118 logging.critical("Message: " + str(exc_value))
1119 logging.critical("Traceback:")
1121 for line in event.split('\n'):
1122 logging.critical(' '+line)
1123 logging.critical("This message should be considered a bug in the Vend Server.")
1124 logging.critical("Please report this to someone who can fix it.")
1126 logging.warning("Trying again anyway (might not help, but hey...)")