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 = self.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 = ''
724 if self.dispense.isDisabled():
725 logging.info('user '+self.vstatus.cur_user+' is disabled')
726 self.vstatus.mk.set_messages(
727 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
728 self.vstatus.cur_user = ''
729 self.vstatus.cur_pin = ''
735 self.vstatus.cur_pin = ''
736 self.vstatus.mk.set_message('PIN: ')
737 logging.info('need pin for user %s'%self.vstatus.cur_user)
738 self.vstatus.change_state(STATE_GETTING_PIN)
742 Triggered when a key is pressed and the machine is idling.
744 def handle_idle_key(self, event, params):
747 self.vstatus.cur_user = ''
751 self.vstatus.change_state(STATE_GETTING_UID)
752 self.run_handler(event, params)
755 What to do when there is nothing to do.
757 def handle_idle_tick(self, event, params):
759 if self.vstatus.mk.done():
762 if self.vstatus.time_of_next_idler and time() > self.vstatus.time_of_next_idler:
763 self.vstatus.time_of_next_idler = time() + 30
768 self.vstatus.mk.update_display()
770 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK)
771 self.run_handler(event, params)
775 Manages the beeps for the grandfather clock
777 def beep_on(self, when, before=0):
778 start = int(when - before)
782 if now >= start and now <= end:
786 def handle_idle_grandfather_tick(self, event, params):
787 ### check for interesting times
790 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
791 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
792 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
793 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
795 hourfromnow = localtime(time() + 3600)
797 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
798 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
799 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
801 ## check for X seconds to the hour
802 ## if case, update counter to 2
803 if self.beep_on(onthehour,15) \
804 or self.beep_on(halfhour,0) \
805 or self.beep_on(quarterhour,0) \
806 or self.beep_on(threequarterhour,0) \
807 or self.beep_on(fivetothehour,0):
808 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
809 self.run_handler(event, params)
811 self.vstatus.change_state(STATE_IDLE)
813 def handle_grandfather_tick(self, event, params):
817 ### we live in interesting times
820 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
821 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
822 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
823 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
825 hourfromnow = localtime(time() + 3600)
827 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
828 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
829 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
832 #print "when it fashionable to wear a onion on your hip"
834 if self.beep_on(onthehour,15):
836 next_hour=((hourfromnow[3] + 11) % 12) + 1
837 if onthehour - time() < next_hour and onthehour - time() > 0:
838 self.v.beep(0, False)
842 msg.append(("DING!", False, None))
844 msg.append((" DING!", False, None))
845 elif int(onthehour - time()) == 0:
846 self.v.beep(255, False)
847 msg.append((" BONG!", False, None))
848 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
849 elif self.beep_on(halfhour,0):
851 self.v.beep(0, False)
852 msg.append((" HALFHOUR ", False, 50))
853 elif self.beep_on(quarterhour,0):
855 self.v.beep(0, False)
856 msg.append((" QTR HOUR ", False, 50))
857 elif self.beep_on(threequarterhour,0):
859 self.v.beep(0, False)
860 msg.append((" 3 QTR HR ", False, 50))
861 elif self.beep_on(fivetothehour,0):
863 self.v.beep(0, False)
864 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
868 ## check for X seconds to the hour
871 self.vstatus.mk.set_messages(msg)
874 self.vstatus.mk.update_display()
875 ## if no longer case, return to idle
877 ## change idler to be clock
878 if go_idle and self.vstatus.mk.done():
879 self.vstatus.change_state(STATE_IDLE,1)
882 What to do when the door is open.
884 def handle_door_idle(self, event, params):
885 def twiddle(clock,v,wise = 2):
887 v.display("-FEED ME-")
888 elif (clock % 4 == 1+wise):
889 v.display("\\FEED ME/")
890 elif (clock % 4 == 2):
891 v.display("-FEED ME-")
892 elif (clock % 4 == 3-wise):
893 v.display("/FEED ME\\")
895 # don't care right now.
898 if ((now % 60 % 2) == 0):
901 twiddle(now, self.v, wise=0)
904 What to do when the door is opened or closed.
906 def handle_door_event(self, event, params):
907 if params == 0: #door open
908 self.vstatus.change_state(STATE_DOOR_OPENING)
909 logging.warning("Entering open door mode")
910 self.v.display("-FEED ME-")
912 self.vstatus.cur_user = ''
913 self.vstatus.cur_pin = ''
914 elif params == 1: #door closed
915 self.vstatus.change_state(STATE_DOOR_CLOSING)
918 logging.warning('Leaving open door mode')
919 self.v.display("-YUM YUM!-")
922 Triggered when a user swipes their caed, and the machine is logged out.
924 def handle_mifare_event(self, event, params):
926 # Translate card_id into uid.
927 if card_id == None or card_id == self._last_card_id:
930 self._last_card_id = card_id
932 self.dispense.authMifareCard(card_id)
933 logging.info('Mapped card id to uid %s'%self.dispense.getUsername())
934 if not self.dispense.isLoggedIn():
935 self.v.beep(40, False)
936 self.vstatus.mk.set_messages(
937 [(self.center('BAD CARD'), False, 1.0),
938 (self.center('SORRY'), False, 0.5)])
939 self.vstatus.cur_user = ''
940 self.vstatus.cur_pin = ''
941 self._last_card_id = -1
945 elif self.dispense.isDisabled():
946 self.v.beep(40, False)
947 self.vstatus.mk.set_messages(
948 [(self.center('ACCT DISABLED'), False, 1.0),
949 (self.center('SORRY'), False, 0.5)])
950 self.dispense.logOut()
954 self.vstatus.cur_selection = ''
955 self.vstatus.change_state(STATE_GET_SELECTION)
956 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
960 Triggered when a user swipes their card and the machine is logged in.
962 def handle_mifare_add_user_event(self, event, params):
965 # Translate card_id into uid.
966 if card_id == None or card_id == self._last_card_id:
969 self._last_card_id = card_id
971 res = self.dispense.addCard(card_id)
973 if get_uid(card_id) != None:
974 self.vstatus.mk.set_messages(
975 [(self.center('ALREADY'), False, 0.5),
976 (self.center('ENROLLED'), False, 0.5)])
978 logging.info('Enrolling card %s to uid %s (%s)'%(card_id, self.vstatus.cur_user, self.vstatus.username))
979 self.set_card_id(self.vstatus.cur_user, self.card_id)
980 self.vstatus.mk.set_messages(
981 [(self.center('CARD'), False, 0.5),
982 (self.center('ENROLLED'), False, 0.5)])
984 def return_to_idle(self, event, params):
988 Maps what to do when the state changes.
990 def create_state_table(self):
991 self.vstatus.state_table[(STATE_IDLE,TICK,1)] = self.handle_idle_tick
992 self.vstatus.state_table[(STATE_IDLE,KEY,1)] = self.handle_idle_key
993 self.vstatus.state_table[(STATE_IDLE,DOOR,1)] = self.handle_door_event
994 self.vstatus.state_table[(STATE_IDLE,MIFARE,1)] = self.handle_mifare_event
996 self.vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = self.handle_door_idle
997 self.vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = self.handle_door_event
998 self.vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = self.do_nothing
999 self.vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = self.do_nothing
1001 self.vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = self.return_to_idle
1002 self.vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = self.handle_door_event
1003 self.vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = self.do_nothing
1004 self.vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = self.do_nothing
1006 self.vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = self.handle_getting_uid_idle
1007 self.vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = self.handle_door_event
1008 self.vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = self.handle_getting_uid_key
1009 self.vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = self.handle_mifare_event
1011 self.vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = self.handle_getting_pin_idle
1012 self.vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = self.handle_door_event
1013 self.vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = self.handle_getting_pin_key
1014 self.vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = self.handle_mifare_event
1016 self.vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = self.handle_get_selection_idle
1017 self.vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = self.handle_door_event
1018 self.vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = self.handle_get_selection_key
1019 self.vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = self.handle_mifare_add_user_event
1021 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = self.handle_idle_grandfather_tick
1022 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = self.handle_grandfather_tick
1023 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = self.handle_door_event
1024 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = self.handle_door_event
1025 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = self.do_nothing
1026 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = self.do_nothing
1027 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = self.handle_mifare_event
1030 Get what to do on a state change.
1032 def get_state_table_handler(self, state, event, counter):
1033 return self.vstatus.state_table[(state,event,counter)]
1035 def time_to_next_update(self):
1036 idle_update = self.vstatus.time_of_next_idlestep - time()
1037 if not self.vstatus.mk.done() and self.vstatus.mk.next_update is not None:
1038 mk_update = self.vstatus.mk.next_update - time()
1039 if mk_update < idle_update:
1040 idle_update = mk_update
1043 def run_forever(self, rfh, wfh, options, cf):
1044 self.v = VendingMachine(rfh, wfh, USE_MIFARE)
1045 self.dispense = Dispense()
1046 self.vstatus = VendState(self.v)
1047 self.create_state_table()
1049 logging.debug('PING is ' + str(self.v.ping()))
1051 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
1060 except DispenseDatabaseException, e:
1061 logging.error('Database error: '+str(e))
1063 timeout = self.time_to_next_update()
1064 (event, params) = self.v.next_event(timeout)
1065 self.run_handler(event, params)
1067 def run_handler(self, event, params):
1068 handler = self.get_state_table_handler(self.vstatus.state,event,self.vstatus.counter)
1070 handler(event, params)
1073 Connect to the machine.
1075 def connect_to_vend(options, cf):
1078 logging.info('Connecting to vending machine using LAT')
1079 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
1080 rfh, wfh = latclient.get_fh()
1081 elif options.use_serial:
1082 # Open vending machine via serial.
1083 logging.info('Connecting to vending machine using serial')
1084 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
1085 rfh,wfh = serialclient.get_fh()
1087 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
1088 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
1090 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
1091 sock.connect((options.host, options.port))
1092 rfh = sock.makefile('r')
1093 wfh = sock.makefile('w')
1100 Parse arguments from the command line
1103 from optparse import OptionParser
1105 op = OptionParser(usage="%prog [OPTION]...")
1106 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')
1107 op.add_option('--serial', action='store_true', default=False, dest='use_serial', help='use the serial port')
1108 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
1109 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
1110 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
1111 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
1112 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
1113 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
1114 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
1115 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
1116 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
1117 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
1118 options, args = op.parse_args()
1121 op.error('extra command line arguments: ' + ' '.join(args))
1125 def create_pid_file(name):
1127 pid_file = file(name, 'w')
1128 pid_file.write('%d\n'%os.getpid())
1131 logging.warning('unable to write to pid file '+name+': '+str(e))
1134 def do_nothing(signum, stack):
1135 signal.signal(signum, do_nothing)
1136 def stop_server(signum, stack): raise KeyboardInterrupt
1137 signal.signal(signal.SIGHUP, do_nothing)
1138 signal.signal(signal.SIGTERM, stop_server)
1139 signal.signal(signal.SIGINT, stop_server)
1141 options = parse_args()
1142 config_opts = VendConfigFile(options.config_file, config_options)
1143 if options.daemon: become_daemon()
1144 set_up_logging(options)
1145 if options.pid_file != '': create_pid_file(options.pid_file)
1147 return options, config_opts
1149 def clean_up_nicely(options, config_opts):
1150 if options.pid_file != '':
1152 os.unlink(options.pid_file)
1153 logging.debug('Removed pid file '+options.pid_file)
1154 except OSError: pass # if we can't delete it, meh
1156 def set_up_logging(options):
1157 logger = logging.getLogger()
1159 if not options.daemon:
1160 stderr_logger = logging.StreamHandler(sys.stderr)
1161 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1162 logger.addHandler(stderr_logger)
1164 if options.log_file != '':
1166 file_logger = logging.FileHandler(options.log_file)
1167 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1168 logger.addHandler(file_logger)
1170 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1172 if options.syslog != None:
1173 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1174 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1175 logger.addHandler(sys_logger)
1178 logger.setLevel(logging.WARNING)
1179 elif options.verbose:
1180 logger.setLevel(logging.DEBUG)
1182 logger.setLevel(logging.INFO)
1184 def become_daemon():
1185 dev_null = file('/dev/null')
1186 fd = dev_null.fileno()
1195 raise SystemExit('failed to fork: '+str(e))
1197 def do_vend_server(options, config_opts):
1200 rfh, wfh = connect_to_vend(options, config_opts)
1201 except (SerialClientException, socket.error), e:
1202 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1204 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1205 logging.info("Trying again in 5 seconds.")
1209 # run_forever(rfh, wfh, options, config_opts)
1212 vserver = VendServer()
1213 vserver.run_forever(rfh, wfh, options, config_opts)
1214 except VendingException:
1215 logging.error("Connection died, trying again...")
1216 logging.info("Trying again in 5 seconds.")
1220 def main(argv=None):
1221 options, config_opts = set_stuff_up()
1224 logging.warning('Starting Vend Server')
1225 do_vend_server(options, config_opts)
1226 logging.error('Vend Server finished unexpectedly, restarting')
1227 except KeyboardInterrupt:
1228 logging.info("Killed by signal, cleaning up")
1229 clean_up_nicely(options, config_opts)
1230 logging.warning("Vend Server stopped")
1235 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1236 tb = format_tb(exc_traceback, 20)
1239 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1240 logging.critical("Message: " + str(exc_value))
1241 logging.critical("Traceback:")
1243 for line in event.split('\n'):
1244 logging.critical(' '+line)
1245 logging.critical("This message should be considered a bug in the Vend Server.")
1246 logging.critical("Please report this to someone who can fix it.")
1248 logging.warning("Trying again anyway (might not help, but hey...)")
1250 if __name__ == '__main__':