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
168 Show information to the user as to what can be dispensed.
170 def scroll_options(self, username, mk, welcome = False):
171 # If the user has just logged in, show them their balance
174 acct, unused = Popen(['dispense', 'acct', username], close_fds=True, stdout=PIPE).communicate()
175 # this is fucking appalling
176 balance = acct[acct.find("$")+1:acct.find("(")].strip()
178 msg = [(self.center('WELCOME'), False, TEXT_SPEED),
179 (self.center(username), False, TEXT_SPEED),
180 (self.center(balance), False, TEXT_SPEED),]
183 choices = ' '*10+'CHOICES: '
185 # Show what is in the coke machine
187 for i in range(0, 7):
188 args = ('dispense', 'iteminfo', 'coke:%i' % i)
189 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
190 m = re.match("\s*[a-z]+:\d+\s+(\d+)\.(\d\d)\s+([^\n]+)", info)
191 cents = int(m.group(1))*100 + int(m.group(2))
192 cokes.append('%i %i %s' % (i, cents, m.group(3)));
196 (slot_num, price, slot_name) = c.split(' ', 2)
197 if slot_name == 'dead': continue
198 choices += '%s-(%sc)-%s8 '%(slot_name, price, slot_num)
200 # Show the final few options
201 choices += '55-DOOR '
202 choices += 'OR ANOTHER SNACK. '
203 choices += '99 TO READ AGAIN. '
204 choices += 'CHOICE? '
205 msg.append((choices, False, None))
206 # Send it to the display
213 def _check_pin(self, uid, pin):
214 print "_check_pin('",uid,"',---)"
215 if uid != self._pin_uid:
217 info = pwd.getpwuid(uid)
219 logging.info('getting pin for uid %d: user not in password file'%uid)
221 if info.pw_dir == None: return False
222 pinfile = os.path.join(info.pw_dir, '.pin')
226 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
229 logging.info('getting pin for uid %d: .pin has wrong permissions. Fixing.'%uid)
230 os.chmod(pinfile, 0600)
234 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
236 pinstr = f.readline()
238 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
239 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
242 self._pin_pin = pinstr
243 self._pin_uname = info.pw_name
245 pinstr = self._pin_pin
246 if pin == int(pinstr):
247 logging.info("Pin correct for %d",uid)
249 logging.info("Pin incorrect for %d",uid)
250 return pin == int(pinstr)
253 Check if the users account has been disabled
255 def acct_is_disabled(self, name=None):
257 name = self._pin_uname
258 acct, unused = Popen(['dispense', 'acct', self._pin_uname], close_fds=True, stdout=PIPE).communicate()
259 # this is fucking appalling
260 flags = acct[acct.find("(")+1:acct.find(")")].strip()
261 if 'disabled' in flags:
263 if 'internal' in flags:
268 Check that the user has a valid pin set
270 def has_good_pin(self, uid):
271 return self._check_pin(uid, None) != None
274 Verify the users pin.
276 def verify_user_pin(self, uid, pin, skip_pin_check=False):
277 if skip_pin_check or self._check_pin(uid, pin) == True:
278 info = pwd.getpwuid(uid)
280 if self.acct_is_disabled(info.pw_name):
281 logging.info('refused mifare for disabled acct uid %d (%s)'%(uid,info.pw_name))
283 logging.info('accepted mifare for uid %d (%s)'%(uid,info.pw_name))
285 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
288 logging.info('refused pin for uid %d'%(uid))
292 In here just for fun.
296 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
297 choice = int(random()*len(messages))
298 msg = messages[choice]
299 left = range(len(msg))
300 for i in range(len(msg)):
301 if msg[i] == ' ': left.remove(i)
305 for i in range(0, len(msg)):
311 s += chr(int(random()*26)+ord('A'))
319 Format text so it will appear centered on the screen.
321 def center(self, str):
323 return ' '*((LEN-len(str))/2)+str
326 Configure the things that will appear on screen whil the machine is idling.
328 def setup_idlers(self):
332 GrayIdler(self.v,one="*",zero="-"),
333 GrayIdler(self.v,one="/",zero="\\"),
334 GrayIdler(self.v,one="X",zero="O"),
335 GrayIdler(self.v,one="*",zero="-",reorder=1),
336 GrayIdler(self.v,one="/",zero="\\",reorder=1),
337 GrayIdler(self.v,one="X",zero="O",reorder=1),
343 StringIdler(self.v), # Hello Cruel World
344 StringIdler(self.v, text="Kill 'em all", repeat=False),
345 StringIdler(self.v, text=CREDITS),
346 StringIdler(self.v, text=str(math.pi) + " "),
347 StringIdler(self.v, text=str(math.e) + " "),
348 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),
349 # "Hello World" in brainfuck
350 StringIdler(self.v, text=">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++."),
354 FileIdler(self.v, '/usr/share/common-licenses/GPL-2',affinity=2),
356 PipeIdler(self.v, "/usr/bin/getent", "passwd"),
357 FortuneIdler(self.v,affinity=20),
363 Go back to the default idler.
365 def reset_idler(self, t = None):
366 self.idler = GreetingIdler(self.v, t)
367 self.vstatus.time_of_next_idlestep = time()+self.idler.next()
368 self.vstatus.time_of_next_idler = None
369 self.vstatus.time_to_autologout = None
370 self.vstatus.change_state(STATE_IDLE, 1)
373 Change to a random idler.
375 def choose_idler(self):
377 # Implementation of the King Of the Hill algorithm from;
378 # http://eli.thegreenplace.net/2010/01/22/weighted-random-generation-in-python/
380 #def weighted_choice_king(weights):
383 # for i, w in enumerate(weights):
385 # if random.random() * total < w:
393 for choice in self.idlers:
394 weight = choice.affinity()
396 if random() * total < weight:
404 Run every step while the machine is idling.
407 if self.idler.finished():
409 self.vstatus.time_of_next_idler = time() + 30
410 nextidle = self.idler.next()
412 nextidle = IDLE_SPEED
413 self.vstatus.time_of_next_idlestep = time()+nextidle
416 These next two events trigger no response in the code.
418 def handle_tick_event(self, event, params):
419 # don't care right now.
422 def handle_switch_event(self, event, params):
423 # don't care right now.
427 Don't do anything for this event.
429 def do_nothing(self, event, params):
430 print "doing nothing (s,e,p)", state, " ", event, " ", params
434 These next few entrie tell us to do nothing while we are idling
436 def handle_getting_uid_idle(self, event, params):
437 # don't care right now.
440 def handle_getting_pin_idle(self, event, params):
441 # don't care right now.
445 While logged in and waiting for user input, slowly get closer to logging out.
447 def handle_get_selection_idle(self, event, params):
448 # don't care right now.
450 ### State logging out ..
451 if self.vstatus.time_to_autologout != None:
452 time_left = self.vstatus.time_to_autologout - time()
453 if time_left < 6 and (self.vstatus.last_timeout_refresh is None or self.vstatus.last_timeout_refresh > time_left):
454 self.vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
455 self.vstatus.last_timeout_refresh = int(time_left)
456 self.vstatus.cur_selection = ''
458 if self.vstatus.time_to_autologout != None and self.vstatus.time_to_autologout - time() <= 0:
459 self.vstatus.time_to_autologout = None
460 self.vstatus.cur_user = ''
461 self.vstatus.cur_pin = ''
462 self.vstatus.cur_selection = ''
463 self._last_card_id = -1
466 ### State fully logged out ... reset variables
467 if self.vstatus.time_to_autologout and not self.vstatus.mk.done():
468 self.vstatus.time_to_autologout = None
469 if self.vstatus.cur_user == '' and self.vstatus.time_to_autologout:
470 self.vstatus.time_to_autologout = None
473 if len(self.vstatus.cur_pin) == PIN_LENGTH and self.vstatus.mk.done() and self.vstatus.time_to_autologout == None:
475 self.vstatus.time_to_autologout = time() + 15
476 self.vstatus.last_timeout_refresh = None
478 ## FIXME - this may need to be elsewhere.....
480 self.vstatus.mk.update_display()
483 Triggered on user input while logged in.
485 def handle_get_selection_key(self, event, params):
487 if len(self.vstatus.cur_selection) == 0:
489 self.vstatus.cur_pin = ''
490 self.vstatus.cur_user = ''
491 self.vstatus.cur_selection = ''
493 self.vstatus.mk.set_messages([(self.center('BYE!'), False, 1.5)])
496 self.vstatus.cur_selection += chr(key + ord('0'))
497 self.vstatus.mk.set_message('SELECT: '+self.vstatus.cur_selection)
498 self.vstatus.time_to_autologout = None
499 elif len(self.vstatus.cur_selection) == 1:
501 self.vstatus.cur_selection = ''
502 self.vstatus.time_to_autologout = None
503 self.scroll_options(self.vstatus.username, self.vstatus.mk)
506 self.vstatus.cur_selection += chr(key + ord('0'))
507 if self.vstatus.cur_user:
508 self.make_selection()
509 self.vstatus.cur_selection = ''
510 self.vstatus.time_to_autologout = time() + 8
511 self.vstatus.last_timeout_refresh = None
515 self.vstatus.cur_selection = ''
516 self.vstatus.time_to_autologout = None
517 self.vstatus.last_timeout_refresh = None
520 Triggered when the user has entered the id of something they would like to purchase.
522 def make_selection(self):
523 # should use sudo here
524 if self.vstatus.cur_selection == '55':
525 self.vstatus.mk.set_message('OPENSESAME')
526 logging.info('dispensing a door for %s'%self.vstatus.username)
528 ret = os.system('dispense -u "%s" door'%self.vstatus.username)
530 ret = os.system('dispense door')
532 logging.info('door opened')
533 self.vstatus.mk.set_message(self.center('DOOR OPEN'))
535 logging.warning('user %s tried to dispense a bad door'%self.vstatus.username)
536 self.vstatus.mk.set_message(self.center('BAD DOOR'))
538 elif self.vstatus.cur_selection == '81':
540 elif self.vstatus.cur_selection == '99':
541 self.scroll_options(self.vstatus.username, self.vstatus.mk)
542 self.vstatus.cur_selection = ''
544 elif self.vstatus.cur_selection[1] == '8':
545 self.v.display('GOT DRINK?')
546 if ((os.system('dispense -u "%s" coke:%s'%(self.vstatus.username, self.vstatus.cur_selection[0])) >> 8) != 0):
547 self.v.display('SEEMS NOT')
549 self.v.display('GOT DRINK!')
551 # first see if it's a named slot
553 price, shortname, name = get_snack( self.vstatus.cur_selection )
555 price, shortname, name = get_snack( '--' )
556 dollarprice = "$%.2f" % ( price / 100.0 )
557 self.v.display(self.vstatus.cur_selection+' - %s'%dollarprice)
558 exitcode = os.system('dispense -u "%s" snack:%s'%(self.vstatus.username, self.vstatus.cur_selection)) >> 8
560 # magic dispense syslog service
561 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, self.vstatus.cur_selection, self.vstatus.username))
562 (worked, code, string) = self.v.vend(self.vstatus.cur_selection)
564 self.v.display('THANK YOU')
566 print "Vend Failed:", code, string
567 self.v.display('VEND FAIL')
568 elif (exitcode == 5): # RV_BALANCE
569 self.v.display('NO MONEY?')
570 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
571 self.v.display('EMPTY SLOT')
572 elif (exitcode == 1): # RV_BADITEM (Dead slot)
573 self.v.display('EMPTY SLOT')
575 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))
576 self.v.display('UNK ERROR')
580 Find the price of an item.
583 if self.vstatus.cur_selection[1] == '8':
584 args = ('dispense', 'iteminfo', 'coke:' + self.vstatus.cur_selection[0])
585 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
586 dollarprice = re.match("\s*[a-z]+:\d+\s+(\d+\.\d\d)\s+([^\n]+)", info).group(1)
588 # first see if it's a named slot
590 price, shortname, name = get_snack( self.vstatus.cur_selection )
592 price, shortname, name = get_snack( '--' )
593 dollarprice = "$%.2f" % ( price / 100.0 )
594 self.v.display(self.vstatus.cur_selection+' - %s'%dollarprice)
597 Triggered when the user presses a button while entering their pin.
599 def handle_getting_pin_key(self, event, params):
601 if len(self.vstatus.cur_pin) < PIN_LENGTH:
603 if self.vstatus.cur_pin == '':
604 self.vstatus.cur_user = ''
608 self.vstatus.cur_pin = ''
609 self.vstatus.mk.set_message('PIN: ')
611 self.vstatus.cur_pin += chr(key + ord('0'))
612 self.vstatus.mk.set_message('PIN: '+'X'*len(self.vstatus.cur_pin))
613 if len(self.vstatus.cur_pin) == PIN_LENGTH:
614 self.vstatus.username = self.verify_user_pin(int(self.vstatus.cur_user), int(self.vstatus.cur_pin))
615 if self.vstatus.username:
616 self.v.beep(0, False)
617 self.vstatus.cur_selection = ''
618 self.vstatus.change_state(STATE_GET_SELECTION)
619 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
622 self.v.beep(40, False)
623 self.vstatus.mk.set_messages(
624 [(self.center('BAD PIN'), False, 1.0),
625 (self.center('SORRY'), False, 0.5)])
626 self.vstatus.cur_user = ''
627 self.vstatus.cur_pin = ''
634 Triggered when the user presses a button while entering their user id.
636 def handle_getting_uid_key(self, event, params):
638 # complicated key handling here:
640 if len(self.vstatus.cur_user) == 0 and key == 9:
641 self.vstatus.cur_selection = ''
642 self.vstatus.time_to_autologout = None
643 self.vstatus.mk.set_message('PRICECHECK')
645 self.scroll_options('', self.vstatus.mk)
646 self.vstatus.change_state(STATE_GET_SELECTION)
649 if len(self.vstatus.cur_user) <8:
651 self.vstatus.cur_user = ''
655 self.vstatus.cur_user += chr(key + ord('0'))
656 #logging.info('dob: '+vstatus.cur_user)
657 if len(self.vstatus.cur_user) > 5:
658 self.vstatus.mk.set_message('>'+self.vstatus.cur_user)
660 self.vstatus.mk.set_message('UID: '+self.vstatus.cur_user)
662 if len(self.vstatus.cur_user) == 5:
663 uid = int(self.vstatus.cur_user)
666 logging.info('user '+self.vstatus.cur_user+' has a bad PIN')
672 Welcome to Picklevision Sytems, Sunnyvale, CA
674 Greetings Professor Falken.
679 Shall we play a game?
682 Please choose from the following menu:
689 6. Toxic and Biochemical Warfare
690 7. Global Thermonuclear War
694 Wouldn't you prefer a nice game of chess?
696 """.replace('\n',' ')
697 self.vstatus.mk.set_messages([(pfalken, False, 10)])
698 self.vstatus.cur_user = ''
699 self.vstatus.cur_pin = ''
705 if not self.has_good_pin(uid):
706 logging.info('user '+self.vstatus.cur_user+' has a bad PIN')
707 self.vstatus.mk.set_messages(
708 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
709 self.vstatus.cur_user = ''
710 self.vstatus.cur_pin = ''
716 if self.acct_is_disabled():
717 logging.info('user '+self.vstatus.cur_user+' is disabled')
718 self.vstatus.mk.set_messages(
719 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
720 self.vstatus.cur_user = ''
721 self.vstatus.cur_pin = ''
727 self.vstatus.cur_pin = ''
728 self.vstatus.mk.set_message('PIN: ')
729 logging.info('need pin for user %s'%self.vstatus.cur_user)
730 self.vstatus.change_state(STATE_GETTING_PIN)
734 Triggered when a key is pressed and the machine is idling.
736 def handle_idle_key(self, event, params):
739 self.vstatus.cur_user = ''
743 self.vstatus.change_state(STATE_GETTING_UID)
744 self.run_handler(event, params)
747 What to do when there is nothing to do.
749 def handle_idle_tick(self, event, params):
751 if self.vstatus.mk.done():
754 if self.vstatus.time_of_next_idler and time() > self.vstatus.time_of_next_idler:
755 self.vstatus.time_of_next_idler = time() + 30
760 self.vstatus.mk.update_display()
762 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK)
763 self.run_handler(event, params)
767 Manages the beeps for the grandfather clock
769 def beep_on(self, when, before=0):
770 start = int(when - before)
774 if now >= start and now <= end:
778 def handle_idle_grandfather_tick(self, event, params):
779 ### check for interesting times
782 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
783 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
784 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
785 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
787 hourfromnow = localtime(time() + 3600)
789 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
790 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
791 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
793 ## check for X seconds to the hour
794 ## if case, update counter to 2
795 if self.beep_on(onthehour,15) \
796 or self.beep_on(halfhour,0) \
797 or self.beep_on(quarterhour,0) \
798 or self.beep_on(threequarterhour,0) \
799 or self.beep_on(fivetothehour,0):
800 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
801 self.run_handler(event, params)
803 self.vstatus.change_state(STATE_IDLE)
805 def handle_grandfather_tick(self, event, params):
809 ### we live in interesting times
812 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
813 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
814 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
815 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
817 hourfromnow = localtime(time() + 3600)
819 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
820 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
821 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
824 #print "when it fashionable to wear a onion on your hip"
826 if self.beep_on(onthehour,15):
828 next_hour=((hourfromnow[3] + 11) % 12) + 1
829 if onthehour - time() < next_hour and onthehour - time() > 0:
830 self.v.beep(0, False)
834 msg.append(("DING!", False, None))
836 msg.append((" DING!", False, None))
837 elif int(onthehour - time()) == 0:
838 self.v.beep(255, False)
839 msg.append((" BONG!", False, None))
840 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
841 elif self.beep_on(halfhour,0):
843 self.v.beep(0, False)
844 msg.append((" HALFHOUR ", False, 50))
845 elif self.beep_on(quarterhour,0):
847 self.v.beep(0, False)
848 msg.append((" QTR HOUR ", False, 50))
849 elif self.beep_on(threequarterhour,0):
851 self.v.beep(0, False)
852 msg.append((" 3 QTR HR ", False, 50))
853 elif self.beep_on(fivetothehour,0):
855 self.v.beep(0, False)
856 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
860 ## check for X seconds to the hour
863 self.vstatus.mk.set_messages(msg)
866 self.vstatus.mk.update_display()
867 ## if no longer case, return to idle
869 ## change idler to be clock
870 if go_idle and self.vstatus.mk.done():
871 self.vstatus.change_state(STATE_IDLE,1)
874 What to do when the door is open.
876 def handle_door_idle(self, event, params):
877 def twiddle(clock,v,wise = 2):
879 v.display("-FEED ME-")
880 elif (clock % 4 == 1+wise):
881 v.display("\\FEED ME/")
882 elif (clock % 4 == 2):
883 v.display("-FEED ME-")
884 elif (clock % 4 == 3-wise):
885 v.display("/FEED ME\\")
887 # don't care right now.
890 if ((now % 60 % 2) == 0):
893 twiddle(now, self.v, wise=0)
896 What to do when the door is opened or closed.
898 def handle_door_event(self, event, params):
899 if params == 0: #door open
900 self.vstatus.change_state(STATE_DOOR_OPENING)
901 logging.warning("Entering open door mode")
902 self.v.display("-FEED ME-")
904 self.vstatus.cur_user = ''
905 self.vstatus.cur_pin = ''
906 elif params == 1: #door closed
907 self.vstatus.change_state(STATE_DOOR_CLOSING)
910 logging.warning('Leaving open door mode')
911 self.v.display("-YUM YUM!-")
914 Triggered when a user swipes their caed, and the machine is logged out.
916 def handle_mifare_event(self, event, params):
918 # Translate card_id into uid.
919 if card_id == None or card_id == self._last_card_id:
922 self._last_card_id = card_id
925 self.vstatus.cur_user = get_uid(card_id)
926 logging.info('Mapped card id to uid %s'%self.vstatus.cur_user)
927 self.vstatus.username = get_uname(self.vstatus.cur_user)
928 if self.acct_is_disabled(self.vstatus.username):
929 self.vstatus.username = '-disabled-'
931 self.vstatus.username = None
932 if self.vstatus.username == '-disabled-':
933 self.v.beep(40, False)
934 self.vstatus.mk.set_messages(
935 [(self.center('ACCT DISABLED'), False, 1.0),
936 (self.center('SORRY'), False, 0.5)])
937 self.vstatus.cur_user = ''
938 self.vstatus.cur_pin = ''
939 self.vstatus.username = None
943 elif self.vstatus.username:
944 self.v.beep(0, False)
945 self.vstatus.cur_selection = ''
946 self.vstatus.change_state(STATE_GET_SELECTION)
947 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
950 self.v.beep(40, False)
951 self.vstatus.mk.set_messages(
952 [(self.center('BAD CARD'), False, 1.0),
953 (self.center('SORRY'), False, 0.5)])
954 self.vstatus.cur_user = ''
955 self.vstatus.cur_pin = ''
956 self._last_card_id = -1
962 Triggered when a user swipes their card and the machine is logged in.
964 def handle_mifare_add_user_event(self, evnt, params):
967 # Translate card_id into uid.
968 if card_id == None or card_id == self._last_card_id:
971 self._last_card_id = card_id
974 if get_uid(card_id) != None:
975 self.vstatus.mk.set_messages(
976 [(self.center('ALREADY'), False, 0.5),
977 (self.center('ENROLLED'), False, 0.5)])
979 # scroll_options(vstatus.username, vstatus.mk)
984 logging.info('Enrolling card %s to uid %s (%s)'%(card_id, self.vstatus.cur_user, self.vstatus.username))
985 self.set_card_id(self.vstatus.cur_user, self.card_id)
986 self.vstatus.mk.set_messages(
987 [(self.center('CARD'), False, 0.5),
988 (self.center('ENROLLED'), False, 0.5)])
990 # scroll_options(vstatus.username, vstatus.mk)
992 def return_to_idle(self, event, params):
996 Maps what to do when the state changes.
998 def create_state_table(self):
999 self.vstatus.state_table[(STATE_IDLE,TICK,1)] = self.handle_idle_tick
1000 self.vstatus.state_table[(STATE_IDLE,KEY,1)] = self.handle_idle_key
1001 self.vstatus.state_table[(STATE_IDLE,DOOR,1)] = self.handle_door_event
1002 self.vstatus.state_table[(STATE_IDLE,MIFARE,1)] = self.handle_mifare_event
1004 self.vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = self.handle_door_idle
1005 self.vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = self.handle_door_event
1006 self.vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = self.do_nothing
1007 self.vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = self.do_nothing
1009 self.vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = self.return_to_idle
1010 self.vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = self.handle_door_event
1011 self.vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = self.do_nothing
1012 self.vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = self.do_nothing
1014 self.vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = self.handle_getting_uid_idle
1015 self.vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = self.handle_door_event
1016 self.vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = self.handle_getting_uid_key
1017 self.vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = self.handle_mifare_event
1019 self.vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = self.handle_getting_pin_idle
1020 self.vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = self.handle_door_event
1021 self.vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = self.handle_getting_pin_key
1022 self.vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = self.handle_mifare_event
1024 self.vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = self.handle_get_selection_idle
1025 self.vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = self.handle_door_event
1026 self.vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = self.handle_get_selection_key
1027 self.vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = self.handle_mifare_add_user_event
1029 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = self.handle_idle_grandfather_tick
1030 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = self.handle_grandfather_tick
1031 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = self.handle_door_event
1032 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = self.handle_door_event
1033 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = self.do_nothing
1034 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = self.do_nothing
1035 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = self.handle_mifare_event
1038 Get what to do on a state change.
1040 def get_state_table_handler(self, state, event, counter):
1041 return self.vstatus.state_table[(state,event,counter)]
1043 def time_to_next_update(self):
1044 idle_update = self.vstatus.time_of_next_idlestep - time()
1045 if not self.vstatus.mk.done() and self.vstatus.mk.next_update is not None:
1046 mk_update = self.vstatus.mk.next_update - time()
1047 if mk_update < idle_update:
1048 idle_update = mk_update
1051 def run_forever(self, rfh, wfh, options, cf):
1052 self.v = VendingMachine(rfh, wfh, USE_MIFARE)
1053 self.vstatus = VendState(self.v)
1054 self.create_state_table()
1056 logging.debug('PING is ' + str(self.v.ping()))
1058 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
1067 except DispenseDatabaseException, e:
1068 logging.error('Database error: '+str(e))
1070 timeout = self.time_to_next_update()
1071 (event, params) = self.v.next_event(timeout)
1072 self.run_handler(event, params)
1074 def run_handler(self, event, params):
1075 handler = self.get_state_table_handler(self.vstatus.state,event,self.vstatus.counter)
1077 handler(event, params)
1080 Connect to the machine.
1082 def connect_to_vend(options, cf):
1085 logging.info('Connecting to vending machine using LAT')
1086 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
1087 rfh, wfh = latclient.get_fh()
1088 elif options.use_serial:
1089 # Open vending machine via serial.
1090 logging.info('Connecting to vending machine using serial')
1091 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
1092 rfh,wfh = serialclient.get_fh()
1094 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
1095 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
1097 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
1098 sock.connect((options.host, options.port))
1099 rfh = sock.makefile('r')
1100 wfh = sock.makefile('w')
1107 Parse arguments from the command line
1110 from optparse import OptionParser
1112 op = OptionParser(usage="%prog [OPTION]...")
1113 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')
1114 op.add_option('--serial', action='store_true', default=False, dest='use_serial', help='use the serial port')
1115 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
1116 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
1117 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
1118 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
1119 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
1120 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
1121 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
1122 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
1123 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
1124 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
1125 options, args = op.parse_args()
1128 op.error('extra command line arguments: ' + ' '.join(args))
1132 def create_pid_file(name):
1134 pid_file = file(name, 'w')
1135 pid_file.write('%d\n'%os.getpid())
1138 logging.warning('unable to write to pid file '+name+': '+str(e))
1141 def do_nothing(signum, stack):
1142 signal.signal(signum, do_nothing)
1143 def stop_server(signum, stack): raise KeyboardInterrupt
1144 signal.signal(signal.SIGHUP, do_nothing)
1145 signal.signal(signal.SIGTERM, stop_server)
1146 signal.signal(signal.SIGINT, stop_server)
1148 options = parse_args()
1149 config_opts = VendConfigFile(options.config_file, config_options)
1150 if options.daemon: become_daemon()
1151 set_up_logging(options)
1152 if options.pid_file != '': create_pid_file(options.pid_file)
1154 return options, config_opts
1156 def clean_up_nicely(options, config_opts):
1157 if options.pid_file != '':
1159 os.unlink(options.pid_file)
1160 logging.debug('Removed pid file '+options.pid_file)
1161 except OSError: pass # if we can't delete it, meh
1163 def set_up_logging(options):
1164 logger = logging.getLogger()
1166 if not options.daemon:
1167 stderr_logger = logging.StreamHandler(sys.stderr)
1168 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1169 logger.addHandler(stderr_logger)
1171 if options.log_file != '':
1173 file_logger = logging.FileHandler(options.log_file)
1174 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1175 logger.addHandler(file_logger)
1177 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1179 if options.syslog != None:
1180 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1181 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1182 logger.addHandler(sys_logger)
1185 logger.setLevel(logging.WARNING)
1186 elif options.verbose:
1187 logger.setLevel(logging.DEBUG)
1189 logger.setLevel(logging.INFO)
1191 def become_daemon():
1192 dev_null = file('/dev/null')
1193 fd = dev_null.fileno()
1202 raise SystemExit('failed to fork: '+str(e))
1204 def do_vend_server(options, config_opts):
1207 rfh, wfh = connect_to_vend(options, config_opts)
1208 except (SerialClientException, socket.error), e:
1209 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1211 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1212 logging.info("Trying again in 5 seconds.")
1216 # run_forever(rfh, wfh, options, config_opts)
1219 vserver = VendServer()
1220 vserver.run_forever(rfh, wfh, options, config_opts)
1221 except VendingException:
1222 logging.error("Connection died, trying again...")
1223 logging.info("Trying again in 5 seconds.")
1227 def main(argv=None):
1228 options, config_opts = set_stuff_up()
1231 logging.warning('Starting Vend Server')
1232 do_vend_server(options, config_opts)
1233 logging.error('Vend Server finished unexpectedly, restarting')
1234 except KeyboardInterrupt:
1235 logging.info("Killed by signal, cleaning up")
1236 clean_up_nicely(options, config_opts)
1237 logging.warning("Vend Server stopped")
1242 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1243 tb = format_tb(exc_traceback, 20)
1246 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1247 logging.critical("Message: " + str(exc_value))
1248 logging.critical("Traceback:")
1250 for line in event.split('\n'):
1251 logging.critical(' '+line)
1252 logging.critical("This message should be considered a bug in the Vend Server.")
1253 logging.critical("Please report this to someone who can fix it.")
1255 logging.warning("Trying again anyway (might not help, but hey...)")
1257 if __name__ == '__main__':