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 subprocess import Popen, PIPE
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,get_uname, set_card_id
25 from OpenDispense import OpenDispense as Dispense
28 This vending machine software brought to you by:
33 and a collective of hungry alpacas.
35 The MIFARE card reader bought to you by:
38 Bug Hunting and hardware maintenance by:
41 For a good time call +61 8 6488 3901
63 STATE_GRANDFATHER_CLOCK,
71 'DBServer': ('Database', 'Server'),
72 'DBName': ('Database', 'Name'),
73 'DBUser': ('VendingMachine', 'DBUser'),
74 'DBPassword': ('VendingMachine', 'DBPassword'),
76 'ServiceName': ('VendingMachine', 'ServiceName'),
77 'ServicePassword': ('VendingMachine', 'Password'),
79 'ServerName': ('DecServer', 'Name'),
80 'ConnectPassword': ('DecServer', 'ConnectPassword'),
81 'PrivPassword': ('DecServer', 'PrivPassword'),
85 def __init__(self, config_file, options):
87 cp = ConfigParser.ConfigParser()
90 for option in options:
91 section, name = options[option]
92 value = cp.get(section, name)
93 self.__dict__[option] = value
95 except ConfigParser.Error, e:
96 raise SystemExit("Error reading config file "+config_file+": " + str(e))
98 class DispenseDatabaseException(Exception): pass
100 class DispenseDatabase:
101 def __init__(self, vending_machine, host, name, user, password):
102 self.vending_machine = vending_machine
103 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
104 self.db.query('LISTEN vend_requests')
106 def process_requests(self):
107 logging.debug('database processing')
108 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
110 outstanding = self.db.query(query).getresult()
111 except (pg.error,), db_err:
112 raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
113 for (id, slot) in outstanding:
114 (worked, code, string) = self.vending_machine.vend(slot)
115 logging.debug (str((worked, code, string)))
117 query = 'SELECT vend_success(%s)'%id
118 self.db.query(query).getresult()
120 query = 'SELECT vend_failed(%s)'%id
121 self.db.query(query).getresult()
123 def handle_events(self):
124 notifier = self.db.getnotify()
125 while notifier is not None:
126 self.process_requests()
127 notify = self.db.getnotify()
129 This class manages the current state of the vending machine.
132 def __init__(self,v):
133 self.state_table = {}
134 self.state = STATE_IDLE
137 self.mk = MessageKeeper(v)
141 self.cur_selection = ''
142 self.time_to_autologout = None
144 self.last_timeout_refresh = None
146 def change_state(self,newstate,newcounter=None):
147 if self.state != newstate:
148 self.state = newstate
150 if newcounter is not None and self.counter != newcounter:
151 self.counter = newcounter
170 Show information to the user as to what can be dispensed.
172 def scroll_options(self, username, mk, welcome = False):
173 # If the user has just logged in, show them their balance
175 balance = dispense.getBalance()
177 msg = [(self.center('WELCOME'), False, TEXT_SPEED),
178 (self.center(self.dispense.getUsername()), False, TEXT_SPEED),
179 (self.center(balance), False, TEXT_SPEED),]
182 choices = ' '*10+'CHOICES: '
184 # Show what is in the coke machine
185 # Need to update this so it uses the abstracted system
187 for i in ['08', '18', '28', '38', '48', '58', '68']:
188 cokes.append((i, self.dispense.getItemInfo(i)))
191 if c[1][0] == 'dead':
193 choices += '%s-(%sc)-%s8 '%(c[1][0], c[1][1], c[0])
195 # Show the final few options
196 choices += '55-DOOR '
197 choices += 'OR ANOTHER SNACK. '
198 choices += '99 TO READ AGAIN. '
199 choices += 'CHOICE? '
200 msg.append((choices, False, None))
201 # Send it to the display
209 def _check_pin(self, uid, pin):
210 print "_check_pin('",uid,"',---)"
211 if uid != self._pin_uid:
213 info = pwd.getpwuid(uid)
215 logging.info('getting pin for uid %d: user not in password file'%uid)
217 if info.pw_dir == None: return False
218 pinfile = os.path.join(info.pw_dir, '.pin')
222 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
225 logging.info('getting pin for uid %d: .pin has wrong permissions. Fixing.'%uid)
226 os.chmod(pinfile, 0600)
230 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
232 pinstr = f.readline()
234 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
235 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
238 self._pin_pin = pinstr
239 self._pin_uname = info.pw_name
241 pinstr = self._pin_pin
242 if pin == int(pinstr):
243 logging.info("Pin correct for %d",uid)
245 logging.info("Pin incorrect for %d",uid)
246 return pin == int(pinstr)
250 Check if the users account has been disabled
253 def acct_is_disabled(self, name=None):
255 name = self._pin_uname
256 acct, unused = Popen(['dispense', 'acct', self._pin_uname], close_fds=True, stdout=PIPE).communicate()
257 # this is fucking appalling
258 flags = acct[acct.find("(")+1:acct.find(")")].strip()
259 if 'disabled' in flags:
261 if 'internal' in flags:
267 Check that the user has a valid pin set
270 def has_good_pin(self, uid):
271 return self._check_pin(uid, None) != None
275 Verify the users pin.
278 def verify_user_pin(self, uid, pin, skip_pin_check=False):
279 if skip_pin_check or self._check_pin(uid, pin) == True:
280 info = pwd.getpwuid(uid)
282 if self.acct_is_disabled(info.pw_name):
283 logging.info('refused mifare for disabled acct uid %d (%s)'%(uid,info.pw_name))
285 logging.info('accepted mifare for uid %d (%s)'%(uid,info.pw_name))
287 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
290 logging.info('refused pin for uid %d'%(uid))
295 In here just for fun.
299 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
300 choice = int(random()*len(messages))
301 msg = messages[choice]
302 left = range(len(msg))
303 for i in range(len(msg)):
304 if msg[i] == ' ': left.remove(i)
308 for i in range(0, len(msg)):
314 s += chr(int(random()*26)+ord('A'))
322 Format text so it will appear centered on the screen.
324 def center(self, str):
326 return ' '*((LEN-len(str))/2)+str
329 Configure the things that will appear on screen whil the machine is idling.
331 def setup_idlers(self):
335 GrayIdler(self.v,one="*",zero="-"),
336 GrayIdler(self.v,one="/",zero="\\"),
337 GrayIdler(self.v,one="X",zero="O"),
338 GrayIdler(self.v,one="*",zero="-",reorder=1),
339 GrayIdler(self.v,one="/",zero="\\",reorder=1),
340 GrayIdler(self.v,one="X",zero="O",reorder=1),
346 StringIdler(self.v), # Hello Cruel World
347 StringIdler(self.v, text="Kill 'em all", repeat=False),
348 StringIdler(self.v, text=CREDITS),
349 StringIdler(self.v, text=str(math.pi) + " "),
350 StringIdler(self.v, text=str(math.e) + " "),
351 StringIdler(self.v, text=" I want some pizza - please call Broadway Pizza on +61 8 9389 8500 - and order as Quinn - I am getting really hungry", repeat=False),
352 # "Hello World" in brainfuck
353 StringIdler(self.v, text=">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++."),
357 FileIdler(self.v, '/usr/share/common-licenses/GPL-2',affinity=2),
359 PipeIdler(self.v, "/usr/bin/getent", "passwd"),
360 FortuneIdler(self.v,affinity=20),
366 Go back to the default idler.
368 def reset_idler(self, t = None):
369 self.idler = GreetingIdler(self.v, t)
370 self.vstatus.time_of_next_idlestep = time()+self.idler.next()
371 self.vstatus.time_of_next_idler = None
372 self.vstatus.time_to_autologout = None
373 self.vstatus.change_state(STATE_IDLE, 1)
376 Change to a random idler.
378 def choose_idler(self):
380 # Implementation of the King Of the Hill algorithm from;
381 # http://eli.thegreenplace.net/2010/01/22/weighted-random-generation-in-python/
383 #def weighted_choice_king(weights):
386 # for i, w in enumerate(weights):
388 # if random.random() * total < w:
396 for choice in self.idlers:
397 weight = choice.affinity()
399 if random() * total < weight:
407 Run every step while the machine is idling.
410 if self.idler.finished():
412 self.vstatus.time_of_next_idler = time() + 30
413 nextidle = self.idler.next()
415 nextidle = IDLE_SPEED
416 self.vstatus.time_of_next_idlestep = time()+nextidle
419 These next two events trigger no response in the code.
421 def handle_tick_event(self, event, params):
422 # don't care right now.
425 def handle_switch_event(self, event, params):
426 # don't care right now.
430 Don't do anything for this event.
432 def do_nothing(self, event, params):
433 print "doing nothing (s,e,p)", state, " ", event, " ", params
437 These next few entrie tell us to do nothing while we are idling
439 def handle_getting_uid_idle(self, event, params):
440 # don't care right now.
443 def handle_getting_pin_idle(self, event, params):
444 # don't care right now.
448 While logged in and waiting for user input, slowly get closer to logging out.
450 def handle_get_selection_idle(self, event, params):
451 # don't care right now.
453 ### State logging out ..
454 if self.vstatus.time_to_autologout != None:
455 time_left = self.vstatus.time_to_autologout - time()
456 if time_left < 6 and (self.vstatus.last_timeout_refresh is None or self.vstatus.last_timeout_refresh > time_left):
457 self.vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
458 self.vstatus.last_timeout_refresh = int(time_left)
459 self.vstatus.cur_selection = ''
461 if self.vstatus.time_to_autologout != None and self.vstatus.time_to_autologout - time() <= 0:
462 self.vstatus.time_to_autologout = None
463 self.vstatus.cur_user = ''
464 self.vstatus.cur_pin = ''
465 self.vstatus.cur_selection = ''
466 self._last_card_id = -1
469 ### State fully logged out ... reset variables
470 if self.vstatus.time_to_autologout and not self.vstatus.mk.done():
471 self.vstatus.time_to_autologout = None
472 if self.vstatus.cur_user == '' and self.vstatus.time_to_autologout:
473 self.vstatus.time_to_autologout = None
476 if len(self.vstatus.cur_pin) == PIN_LENGTH and self.vstatus.mk.done() and self.vstatus.time_to_autologout == None:
478 self.vstatus.time_to_autologout = time() + 15
479 self.vstatus.last_timeout_refresh = None
481 ## FIXME - this may need to be elsewhere.....
483 self.vstatus.mk.update_display()
486 Triggered on user input while logged in.
488 def handle_get_selection_key(self, event, params):
490 if len(self.vstatus.cur_selection) == 0:
492 self.vstatus.cur_pin = ''
493 self.vstatus.cur_user = ''
494 self.vstatus.cur_selection = ''
496 self.vstatus.mk.set_messages([(self.center('BYE!'), False, 1.5)])
499 self.vstatus.cur_selection += chr(key + ord('0'))
500 self.vstatus.mk.set_message('SELECT: '+self.vstatus.cur_selection)
501 self.vstatus.time_to_autologout = None
502 elif len(self.vstatus.cur_selection) == 1:
504 self.vstatus.cur_selection = ''
505 self.vstatus.time_to_autologout = None
506 self.scroll_options(self.vstatus.username, self.vstatus.mk)
509 self.vstatus.cur_selection += chr(key + ord('0'))
510 if self.vstatus.cur_user:
511 self.make_selection()
512 self.vstatus.cur_selection = ''
513 self.vstatus.time_to_autologout = time() + 8
514 self.vstatus.last_timeout_refresh = None
517 self.dispense.getItemInfo(self.vstatus.cur_selection)
518 self.vstatus.cur_selection = ''
519 self.vstatus.time_to_autologout = None
520 self.vstatus.last_timeout_refresh = None
523 Triggered when the user has entered the id of something they would like to purchase.
525 def make_selection(self):
526 # should use sudo here
527 if self.vstatus.cur_selection == '55':
528 self.vstatus.mk.set_message('OPENSESAME')
529 logging.info('dispensing a door for %s'%self.vstatus.username)
531 ret = os.system('dispense -u "%s" door'%self.vstatus.username)
533 ret = os.system('dispense door')
535 logging.info('door opened')
536 self.vstatus.mk.set_message(self.center('DOOR OPEN'))
538 logging.warning('user %s tried to dispense a bad door'%self.vstatus.username)
539 self.vstatus.mk.set_message(self.center('BAD DOOR'))
541 elif self.vstatus.cur_selection == '81':
543 elif self.vstatus.cur_selection == '99':
544 self.scroll_options(self.vstatus.username, self.vstatus.mk)
545 self.vstatus.cur_selection = ''
547 elif self.vstatus.cur_selection[1] == '8':
548 self.v.display('GOT DRINK?')
549 if ((os.system('dispense -u "%s" coke:%s'%(self.vstatus.username, self.vstatus.cur_selection[0])) >> 8) != 0):
550 self.v.display('SEEMS NOT')
552 self.v.display('GOT DRINK!')
554 # first see if it's a named slot
556 price, shortname, name = get_snack( self.vstatus.cur_selection )
558 price, shortname, name = get_snack( '--' )
559 dollarprice = "$%.2f" % ( price / 100.0 )
560 self.v.display(self.vstatus.cur_selection+' - %s'%dollarprice)
561 exitcode = os.system('dispense -u "%s" snack:%s'%(self.vstatus.username, self.vstatus.cur_selection)) >> 8
563 # magic dispense syslog service
564 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, self.vstatus.cur_selection, self.vstatus.username))
565 (worked, code, string) = self.v.vend(self.vstatus.cur_selection)
567 self.v.display('THANK YOU')
569 print "Vend Failed:", code, string
570 self.v.display('VEND FAIL')
571 elif (exitcode == 5): # RV_BALANCE
572 self.v.display('NO MONEY?')
573 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
574 self.v.display('EMPTY SLOT')
575 elif (exitcode == 1): # RV_BADITEM (Dead slot)
576 self.v.display('EMPTY SLOT')
578 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, self.vstatus.cur_selection, self.vstatus.username, exitcode))
579 self.v.display('UNK ERROR')
583 Find the price of an item.
587 if self.vstatus.cur_selection[1] == '8':
588 args = ('dispense', 'iteminfo', 'coke:' + self.vstatus.cur_selection[0])
589 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
590 dollarprice = re.match("\s*[a-z]+:\d+\s+(\d+\.\d\d)\s+([^\n]+)", info).group(1)
592 # first see if it's a named slot
594 price, shortname, name = get_snack( self.vstatus.cur_selection )
596 price, shortname, name = get_snack( '--' )
597 dollarprice = "$%.2f" % ( price / 100.0 )
598 self.v.display(self.vstatus.cur_selection+' - %s'%dollarprice)
602 Triggered when the user presses a button while entering their pin.
604 def handle_getting_pin_key(self, event, params):
606 if len(self.vstatus.cur_pin) < PIN_LENGTH:
608 if self.vstatus.cur_pin == '':
609 self.vstatus.cur_user = ''
613 self.vstatus.cur_pin = ''
614 self.vstatus.mk.set_message('PIN: ')
616 self.vstatus.cur_pin += chr(key + ord('0'))
617 self.vstatus.mk.set_message('PIN: '+'X'*len(self.vstatus.cur_pin))
618 if len(self.vstatus.cur_pin) == PIN_LENGTH:
619 self.dispense.authUserIdPin(self.vstatus.cur_user, self.vstatus.cur_pin)
620 if self.dispense.getUsername():
621 self.v.beep(0, False)
622 self.vstatus.cur_selection = ''
623 self.vstatus.change_state(STATE_GET_SELECTION)
624 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
627 self.v.beep(40, False)
628 self.vstatus.mk.set_messages(
629 [(self.center('BAD PIN'), False, 1.0),
630 (self.center('SORRY'), False, 0.5)])
631 self.vstatus.cur_user = ''
632 self.vstatus.cur_pin = ''
639 Triggered when the user presses a button while entering their user id.
641 def handle_getting_uid_key(self, event, params):
643 # complicated key handling here:
645 if len(self.vstatus.cur_user) == 0 and key == 9:
646 self.vstatus.cur_selection = ''
647 self.vstatus.time_to_autologout = None
648 self.vstatus.mk.set_message('PRICECHECK')
650 self.scroll_options('', self.vstatus.mk)
651 self.vstatus.change_state(STATE_GET_SELECTION)
654 if len(self.vstatus.cur_user) <8:
656 self.vstatus.cur_user = ''
660 self.vstatus.cur_user += chr(key + ord('0'))
661 #logging.info('dob: '+vstatus.cur_user)
662 if len(self.vstatus.cur_user) > 5:
663 self.vstatus.mk.set_message('>'+self.vstatus.cur_user)
665 self.vstatus.mk.set_message('UID: '+self.vstatus.cur_user)
667 if len(self.vstatus.cur_user) == 5:
668 uid = int(self.vstatus.cur_user)
671 logging.info('user '+self.vstatus.cur_user+' has a bad PIN')
677 Welcome to Picklevision Sytems, Sunnyvale, CA
679 Greetings Professor Falken.
684 Shall we play a game?
687 Please choose from the following menu:
694 6. Toxic and Biochemical Warfare
695 7. Global Thermonuclear War
699 Wouldn't you prefer a nice game of chess?
701 """.replace('\n',' ')
702 self.vstatus.mk.set_messages([(pfalken, False, 10)])
703 self.vstatus.cur_user = ''
704 self.vstatus.cur_pin = ''
711 if not self.has_good_pin(uid):
712 logging.info('user '+self.vstatus.cur_user+' has a bad PIN')
713 self.vstatus.mk.set_messages(
714 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
715 self.vstatus.cur_user = ''
716 self.vstatus.cur_pin = ''
723 if self.dispense.isDisabled():
724 logging.info('user '+self.vstatus.cur_user+' is disabled')
725 self.vstatus.mk.set_messages(
726 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
727 self.vstatus.cur_user = ''
728 self.vstatus.cur_pin = ''
734 self.vstatus.cur_pin = ''
735 self.vstatus.mk.set_message('PIN: ')
736 logging.info('need pin for user %s'%self.vstatus.cur_user)
737 self.vstatus.change_state(STATE_GETTING_PIN)
741 Triggered when a key is pressed and the machine is idling.
743 def handle_idle_key(self, event, params):
746 self.vstatus.cur_user = ''
750 self.vstatus.change_state(STATE_GETTING_UID)
751 self.run_handler(event, params)
754 What to do when there is nothing to do.
756 def handle_idle_tick(self, event, params):
758 if self.vstatus.mk.done():
761 if self.vstatus.time_of_next_idler and time() > self.vstatus.time_of_next_idler:
762 self.vstatus.time_of_next_idler = time() + 30
767 self.vstatus.mk.update_display()
769 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK)
770 self.run_handler(event, params)
774 Manages the beeps for the grandfather clock
776 def beep_on(self, when, before=0):
777 start = int(when - before)
781 if now >= start and now <= end:
785 def handle_idle_grandfather_tick(self, event, params):
786 ### check for interesting times
789 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
790 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
791 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
792 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
794 hourfromnow = localtime(time() + 3600)
796 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
797 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
798 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
800 ## check for X seconds to the hour
801 ## if case, update counter to 2
802 if self.beep_on(onthehour,15) \
803 or self.beep_on(halfhour,0) \
804 or self.beep_on(quarterhour,0) \
805 or self.beep_on(threequarterhour,0) \
806 or self.beep_on(fivetothehour,0):
807 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
808 self.run_handler(event, params)
810 self.vstatus.change_state(STATE_IDLE)
812 def handle_grandfather_tick(self, event, params):
816 ### we live in interesting times
819 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
820 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
821 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
822 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
824 hourfromnow = localtime(time() + 3600)
826 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
827 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
828 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
831 #print "when it fashionable to wear a onion on your hip"
833 if self.beep_on(onthehour,15):
835 next_hour=((hourfromnow[3] + 11) % 12) + 1
836 if onthehour - time() < next_hour and onthehour - time() > 0:
837 self.v.beep(0, False)
841 msg.append(("DING!", False, None))
843 msg.append((" DING!", False, None))
844 elif int(onthehour - time()) == 0:
845 self.v.beep(255, False)
846 msg.append((" BONG!", False, None))
847 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
848 elif self.beep_on(halfhour,0):
850 self.v.beep(0, False)
851 msg.append((" HALFHOUR ", False, 50))
852 elif self.beep_on(quarterhour,0):
854 self.v.beep(0, False)
855 msg.append((" QTR HOUR ", False, 50))
856 elif self.beep_on(threequarterhour,0):
858 self.v.beep(0, False)
859 msg.append((" 3 QTR HR ", False, 50))
860 elif self.beep_on(fivetothehour,0):
862 self.v.beep(0, False)
863 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
867 ## check for X seconds to the hour
870 self.vstatus.mk.set_messages(msg)
873 self.vstatus.mk.update_display()
874 ## if no longer case, return to idle
876 ## change idler to be clock
877 if go_idle and self.vstatus.mk.done():
878 self.vstatus.change_state(STATE_IDLE,1)
881 What to do when the door is open.
883 def handle_door_idle(self, event, params):
884 def twiddle(clock,v,wise = 2):
886 v.display("-FEED ME-")
887 elif (clock % 4 == 1+wise):
888 v.display("\\FEED ME/")
889 elif (clock % 4 == 2):
890 v.display("-FEED ME-")
891 elif (clock % 4 == 3-wise):
892 v.display("/FEED ME\\")
894 # don't care right now.
897 if ((now % 60 % 2) == 0):
900 twiddle(now, self.v, wise=0)
903 What to do when the door is opened or closed.
905 def handle_door_event(self, event, params):
906 if params == 0: #door open
907 self.vstatus.change_state(STATE_DOOR_OPENING)
908 logging.warning("Entering open door mode")
909 self.v.display("-FEED ME-")
911 self.vstatus.cur_user = ''
912 self.vstatus.cur_pin = ''
913 elif params == 1: #door closed
914 self.vstatus.change_state(STATE_DOOR_CLOSING)
917 logging.warning('Leaving open door mode')
918 self.v.display("-YUM YUM!-")
921 Triggered when a user swipes their caed, and the machine is logged out.
923 def handle_mifare_event(self, event, params):
925 # Translate card_id into uid.
926 if card_id == None or card_id == self._last_card_id:
929 self._last_card_id = card_id
931 self.dispense.authMifareCard(card_id)
932 logging.info('Mapped card id to uid %s'%self.dispense.getUsername())
933 if not self.dispense.isLoggedIn():
934 self.v.beep(40, False)
935 self.vstatus.mk.set_messages(
936 [(self.center('BAD CARD'), False, 1.0),
937 (self.center('SORRY'), False, 0.5)])
938 self.vstatus.cur_user = ''
939 self.vstatus.cur_pin = ''
940 self._last_card_id = -1
944 elif self.dispense.isDisabled():
945 self.v.beep(40, False)
946 self.vstatus.mk.set_messages(
947 [(self.center('ACCT DISABLED'), False, 1.0),
948 (self.center('SORRY'), False, 0.5)])
949 self.dispense.logOut()
953 self.vstatus.cur_selection = ''
954 self.vstatus.change_state(STATE_GET_SELECTION)
955 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
959 Triggered when a user swipes their card and the machine is logged in.
961 def handle_mifare_add_user_event(self, event, params):
964 # Translate card_id into uid.
965 if card_id == None or card_id == self._last_card_id:
968 self._last_card_id = card_id
970 res = self.dispense.addCard(card_id)
972 if get_uid(card_id) != None:
973 self.vstatus.mk.set_messages(
974 [(self.center('ALREADY'), False, 0.5),
975 (self.center('ENROLLED'), False, 0.5)])
977 logging.info('Enrolling card %s to uid %s (%s)'%(card_id, self.vstatus.cur_user, self.vstatus.username))
978 self.set_card_id(self.vstatus.cur_user, self.card_id)
979 self.vstatus.mk.set_messages(
980 [(self.center('CARD'), False, 0.5),
981 (self.center('ENROLLED'), False, 0.5)])
983 def return_to_idle(self, event, params):
987 Maps what to do when the state changes.
989 def create_state_table(self):
990 self.vstatus.state_table[(STATE_IDLE,TICK,1)] = self.handle_idle_tick
991 self.vstatus.state_table[(STATE_IDLE,KEY,1)] = self.handle_idle_key
992 self.vstatus.state_table[(STATE_IDLE,DOOR,1)] = self.handle_door_event
993 self.vstatus.state_table[(STATE_IDLE,MIFARE,1)] = self.handle_mifare_event
995 self.vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = self.handle_door_idle
996 self.vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = self.handle_door_event
997 self.vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = self.do_nothing
998 self.vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = self.do_nothing
1000 self.vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = self.return_to_idle
1001 self.vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = self.handle_door_event
1002 self.vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = self.do_nothing
1003 self.vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = self.do_nothing
1005 self.vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = self.handle_getting_uid_idle
1006 self.vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = self.handle_door_event
1007 self.vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = self.handle_getting_uid_key
1008 self.vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = self.handle_mifare_event
1010 self.vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = self.handle_getting_pin_idle
1011 self.vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = self.handle_door_event
1012 self.vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = self.handle_getting_pin_key
1013 self.vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = self.handle_mifare_event
1015 self.vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = self.handle_get_selection_idle
1016 self.vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = self.handle_door_event
1017 self.vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = self.handle_get_selection_key
1018 self.vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = self.handle_mifare_add_user_event
1020 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = self.handle_idle_grandfather_tick
1021 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = self.handle_grandfather_tick
1022 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = self.handle_door_event
1023 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = self.handle_door_event
1024 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = self.do_nothing
1025 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = self.do_nothing
1026 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = self.handle_mifare_event
1029 Get what to do on a state change.
1031 def get_state_table_handler(self, state, event, counter):
1032 return self.vstatus.state_table[(state,event,counter)]
1034 def time_to_next_update(self):
1035 idle_update = self.vstatus.time_of_next_idlestep - time()
1036 if not self.vstatus.mk.done() and self.vstatus.mk.next_update is not None:
1037 mk_update = self.vstatus.mk.next_update - time()
1038 if mk_update < idle_update:
1039 idle_update = mk_update
1042 def run_forever(self, rfh, wfh, options, cf):
1043 self.v = VendingMachine(rfh, wfh, USE_MIFARE)
1044 self.dispense = Dispense()
1045 self.vstatus = VendState(self.v)
1046 self.create_state_table()
1048 logging.debug('PING is ' + str(self.v.ping()))
1050 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
1059 except DispenseDatabaseException, e:
1060 logging.error('Database error: '+str(e))
1062 timeout = self.time_to_next_update()
1063 (event, params) = self.v.next_event(timeout)
1064 self.run_handler(event, params)
1066 def run_handler(self, event, params):
1067 handler = self.get_state_table_handler(self.vstatus.state,event,self.vstatus.counter)
1069 handler(event, params)
1072 Connect to the machine.
1074 def connect_to_vend(options, cf):
1077 logging.info('Connecting to vending machine using LAT')
1078 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
1079 rfh, wfh = latclient.get_fh()
1080 elif options.use_serial:
1081 # Open vending machine via serial.
1082 logging.info('Connecting to vending machine using serial')
1083 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
1084 rfh,wfh = serialclient.get_fh()
1086 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
1087 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
1089 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
1090 sock.connect((options.host, options.port))
1091 rfh = sock.makefile('r')
1092 wfh = sock.makefile('w')
1099 Parse arguments from the command line
1102 from optparse import OptionParser
1104 op = OptionParser(usage="%prog [OPTION]...")
1105 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')
1106 op.add_option('--serial', action='store_true', default=False, dest='use_serial', help='use the serial port')
1107 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
1108 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
1109 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
1110 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
1111 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
1112 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
1113 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
1114 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
1115 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
1116 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
1117 options, args = op.parse_args()
1120 op.error('extra command line arguments: ' + ' '.join(args))
1124 def create_pid_file(name):
1126 pid_file = file(name, 'w')
1127 pid_file.write('%d\n'%os.getpid())
1130 logging.warning('unable to write to pid file '+name+': '+str(e))
1133 def do_nothing(signum, stack):
1134 signal.signal(signum, do_nothing)
1135 def stop_server(signum, stack): raise KeyboardInterrupt
1136 signal.signal(signal.SIGHUP, do_nothing)
1137 signal.signal(signal.SIGTERM, stop_server)
1138 signal.signal(signal.SIGINT, stop_server)
1140 options = parse_args()
1141 config_opts = VendConfigFile(options.config_file, config_options)
1142 if options.daemon: become_daemon()
1143 set_up_logging(options)
1144 if options.pid_file != '': create_pid_file(options.pid_file)
1146 return options, config_opts
1148 def clean_up_nicely(options, config_opts):
1149 if options.pid_file != '':
1151 os.unlink(options.pid_file)
1152 logging.debug('Removed pid file '+options.pid_file)
1153 except OSError: pass # if we can't delete it, meh
1155 def set_up_logging(options):
1156 logger = logging.getLogger()
1158 if not options.daemon:
1159 stderr_logger = logging.StreamHandler(sys.stderr)
1160 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1161 logger.addHandler(stderr_logger)
1163 if options.log_file != '':
1165 file_logger = logging.FileHandler(options.log_file)
1166 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1167 logger.addHandler(file_logger)
1169 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1171 if options.syslog != None:
1172 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1173 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1174 logger.addHandler(sys_logger)
1177 logger.setLevel(logging.WARNING)
1178 elif options.verbose:
1179 logger.setLevel(logging.DEBUG)
1181 logger.setLevel(logging.INFO)
1183 def become_daemon():
1184 dev_null = file('/dev/null')
1185 fd = dev_null.fileno()
1194 raise SystemExit('failed to fork: '+str(e))
1196 def do_vend_server(options, config_opts):
1199 rfh, wfh = connect_to_vend(options, config_opts)
1200 except (SerialClientException, socket.error), e:
1201 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1203 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1204 logging.info("Trying again in 5 seconds.")
1208 # run_forever(rfh, wfh, options, config_opts)
1211 vserver = VendServer()
1212 vserver.run_forever(rfh, wfh, options, config_opts)
1213 except VendingException:
1214 logging.error("Connection died, trying again...")
1215 logging.info("Trying again in 5 seconds.")
1219 def main(argv=None):
1220 options, config_opts = set_stuff_up()
1223 logging.warning('Starting Vend Server')
1224 do_vend_server(options, config_opts)
1225 logging.error('Vend Server finished unexpectedly, restarting')
1226 except KeyboardInterrupt:
1227 logging.info("Killed by signal, cleaning up")
1228 clean_up_nicely(options, config_opts)
1229 logging.warning("Vend Server stopped")
1234 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1235 tb = format_tb(exc_traceback, 20)
1238 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1239 logging.critical("Message: " + str(exc_value))
1240 logging.critical("Traceback:")
1242 for line in event.split('\n'):
1243 logging.critical(' '+line)
1244 logging.critical("This message should be considered a bug in the Vend Server.")
1245 logging.critical("Please report this to someone who can fix it.")
1247 logging.warning("Trying again anyway (might not help, but hey...)")
1249 if __name__ == '__main__':