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,
79 'DBServer': ('Database', 'Server'),
80 'DBName': ('Database', 'Name'),
81 'DBUser': ('VendingMachine', 'DBUser'),
82 'DBPassword': ('VendingMachine', 'DBPassword'),
84 'ServiceName': ('VendingMachine', 'ServiceName'),
85 'ServicePassword': ('VendingMachine', 'Password'),
87 'ServerName': ('DecServer', 'Name'),
88 'ConnectPassword': ('DecServer', 'ConnectPassword'),
89 'PrivPassword': ('DecServer', 'PrivPassword'),
93 def __init__(self, config_file, options):
95 cp = ConfigParser.ConfigParser()
98 for option in options:
99 section, name = options[option]
100 value = cp.get(section, name)
101 self.__dict__[option] = value
103 except ConfigParser.Error, e:
104 raise SystemExit("Error reading config file "+config_file+": " + str(e))
106 class DispenseDatabaseException(Exception): pass
108 class DispenseDatabase:
109 def __init__(self, vending_machine, host, name, user, password):
110 self.vending_machine = vending_machine
111 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
112 self.db.query('LISTEN vend_requests')
114 def process_requests(self):
115 logging.debug('database processing')
116 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
118 outstanding = self.db.query(query).getresult()
119 except (pg.error,), db_err:
120 raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
121 for (id, slot) in outstanding:
122 (worked, code, string) = self.vending_machine.vend(slot)
123 logging.debug (str((worked, code, string)))
125 query = 'SELECT vend_success(%s)'%id
126 self.db.query(query).getresult()
128 query = 'SELECT vend_failed(%s)'%id
129 self.db.query(query).getresult()
131 def handle_events(self):
132 notifier = self.db.getnotify()
133 while notifier is not None:
134 self.process_requests()
135 notify = self.db.getnotify()
137 This class manages the current state of the vending machine.
140 def __init__(self,v):
141 self.state_table = {}
142 self.state = STATE_IDLE
145 self.mk = MessageKeeper(v)
149 self.cur_selection = ''
150 self.time_to_autologout = None
152 self.last_timeout_refresh = None
154 def change_state(self,newstate,newcounter=None):
155 if self.state != newstate:
156 self.state = newstate
158 if newcounter is not None and self.counter != newcounter:
159 self.counter = newcounter
162 Show information to the user as to what can be dispensed.
164 def scroll_options(username, mk, welcome = False):
165 # If the user has just logged in, show them their balance
168 acct, unused = Popen(['dispense', 'acct', username], close_fds=True, stdout=PIPE).communicate()
169 # this is fucking appalling
170 balance = acct[acct.find("$")+1:acct.find("(")].strip()
172 msg = [(center('WELCOME'), False, TEXT_SPEED),
173 (center(username), False, TEXT_SPEED),
174 (center(balance), False, TEXT_SPEED),]
177 choices = ' '*10+'CHOICES: '
179 # Show what is in the coke machine
181 for i in range(0, 7):
182 args = ('dispense', 'iteminfo', 'coke:%i' % i)
183 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
184 m = re.match("\s*[a-z]+:\d+\s+(\d+)\.(\d\d)\s+([^\n]+)", info)
185 cents = int(m.group(1))*100 + int(m.group(2))
186 cokes.append('%i %i %s' % (i, cents, m.group(3)));
190 (slot_num, price, slot_name) = c.split(' ', 2)
191 if slot_name == 'dead': continue
192 choices += '%s-(%sc)-%s8 '%(slot_name, price, slot_num)
194 # Show the final few options
195 choices += '55-DOOR '
196 choices += 'OR ANOTHER SNACK. '
197 choices += '99 TO READ AGAIN. '
198 choices += 'CHOICE? '
199 msg.append((choices, False, None))
200 # Send it to the display
207 def _check_pin(uid, pin):
211 print "_check_pin('",uid,"',---)"
214 info = pwd.getpwuid(uid)
216 logging.info('getting pin for uid %d: user not in password file'%uid)
218 if info.pw_dir == None: return False
219 pinfile = os.path.join(info.pw_dir, '.pin')
223 logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
226 logging.info('getting pin for uid %d: .pin has wrong permissions. Fixing.'%uid)
227 os.chmod(pinfile, 0600)
231 logging.info('getting pin for uid %d: I cannot read pin file'%uid)
233 pinstr = f.readline()
235 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
236 logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
240 _pin_uname = info.pw_name
243 if pin == int(pinstr):
244 logging.info("Pin correct for %d",uid)
246 logging.info("Pin incorrect for %d",uid)
247 return pin == int(pinstr)
250 Check if the users account has been disabled
252 def acct_is_disabled(name=None):
256 acct, unused = Popen(['dispense', 'acct', _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:
266 Check that the user has a valid pin set
268 def has_good_pin(uid):
269 return _check_pin(uid, None) != None
272 Verify the users pin.
274 def verify_user_pin(uid, pin, skip_pin_check=False):
275 if skip_pin_check or _check_pin(uid, pin) == True:
276 info = pwd.getpwuid(uid)
278 if acct_is_disabled(info.pw_name):
279 logging.info('refused mifare for disabled acct uid %d (%s)'%(uid,info.pw_name))
281 logging.info('accepted mifare for uid %d (%s)'%(uid,info.pw_name))
283 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
286 logging.info('refused pin for uid %d'%(uid))
290 In here just for fun.
294 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
295 choice = int(random()*len(messages))
296 msg = messages[choice]
297 left = range(len(msg))
298 for i in range(len(msg)):
299 if msg[i] == ' ': left.remove(i)
303 for i in range(0, len(msg)):
309 s += chr(int(random()*26)+ord('A'))
317 Format text so it will appear centered on the screen.
321 return ' '*((LEN-len(str))/2)+str
324 Configure the things that will appear on screen whil the machine is idling.
331 GrayIdler(v,one="*",zero="-"),
332 GrayIdler(v,one="/",zero="\\"),
333 GrayIdler(v,one="X",zero="O"),
334 GrayIdler(v,one="*",zero="-",reorder=1),
335 GrayIdler(v,one="/",zero="\\",reorder=1),
336 GrayIdler(v,one="X",zero="O",reorder=1),
342 StringIdler(v), # Hello Cruel World
343 StringIdler(v, text="Kill 'em all", repeat=False),
344 StringIdler(v, text=CREDITS),
345 StringIdler(v, text=str(math.pi) + " "),
346 StringIdler(v, text=str(math.e) + " "),
347 StringIdler(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),
348 # "Hello World" in brainfuck
349 StringIdler(v, text=">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++."),
353 FileIdler(v, '/usr/share/common-licenses/GPL-2',affinity=2),
355 PipeIdler(v, "/usr/bin/getent", "passwd"),
356 FortuneIdler(v,affinity=20),
362 Go back to the default idler.
364 def reset_idler(v, vstatus, t = None):
366 idler = GreetingIdler(v, t)
367 vstatus.time_of_next_idlestep = time()+idler.next()
368 vstatus.time_of_next_idler = None
369 vstatus.time_to_autologout = None
370 vstatus.change_state(STATE_IDLE, 1)
373 Change to a random idler.
378 # Implementation of the King Of the Hill algorithm from;
379 # http://eli.thegreenplace.net/2010/01/22/weighted-random-generation-in-python/
381 #def weighted_choice_king(weights):
384 # for i, w in enumerate(weights):
386 # if random.random() * total < w:
394 for choice in idlers:
395 weight = choice.affinity()
397 if random() * total < weight:
405 Run every step while the machine is idling.
407 def idle_step(vstatus):
411 vstatus.time_of_next_idler = time() + 30
412 nextidle = idler.next()
414 nextidle = IDLE_SPEED
415 vstatus.time_of_next_idlestep = time()+nextidle
418 These next two events trigger no response in the code.
420 def handle_tick_event(event, params, v, vstatus):
421 # don't care right now.
424 def handle_switch_event(event, params, v, vstatus):
425 # don't care right now.
429 Don't do anything for this event.
431 def do_nothing(state, event, params, v, vstatus):
432 print "doing nothing (s,e,p)", state, " ", event, " ", params
436 These next few entrie tell us to do nothing while we are idling
438 def handle_getting_uid_idle(state, event, params, v, vstatus):
439 # don't care right now.
442 def handle_getting_pin_idle(state, event, params, v, vstatus):
443 # don't care right now.
447 While logged in and waiting for user input, slowly get closer to logging out.
449 def handle_get_selection_idle(state, event, params, v, vstatus):
451 # don't care right now.
453 ### State logging out ..
454 if vstatus.time_to_autologout != None:
455 time_left = vstatus.time_to_autologout - time()
456 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
457 vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
458 vstatus.last_timeout_refresh = int(time_left)
459 vstatus.cur_selection = ''
461 if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
462 vstatus.time_to_autologout = None
463 vstatus.cur_user = ''
465 vstatus.cur_selection = ''
467 reset_idler(v, vstatus)
469 ### State fully logged out ... reset variables
470 if vstatus.time_to_autologout and not vstatus.mk.done():
471 vstatus.time_to_autologout = None
472 if vstatus.cur_user == '' and vstatus.time_to_autologout:
473 vstatus.time_to_autologout = None
476 if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
478 vstatus.time_to_autologout = time() + 15
479 vstatus.last_timeout_refresh = None
481 ## FIXME - this may need to be elsewhere.....
483 vstatus.mk.update_display()
486 Triggered on user input while logged in.
488 def handle_get_selection_key(state, event, params, v, vstatus):
491 if len(vstatus.cur_selection) == 0:
494 vstatus.cur_user = ''
495 vstatus.cur_selection = ''
497 vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
498 reset_idler(v, vstatus, 2)
500 vstatus.cur_selection += chr(key + ord('0'))
501 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
502 vstatus.time_to_autologout = None
503 elif len(vstatus.cur_selection) == 1:
505 vstatus.cur_selection = ''
506 vstatus.time_to_autologout = None
507 scroll_options(vstatus.username, vstatus.mk)
510 vstatus.cur_selection += chr(key + ord('0'))
512 make_selection(v,vstatus)
513 vstatus.cur_selection = ''
514 vstatus.time_to_autologout = time() + 8
515 vstatus.last_timeout_refresh = None
518 price_check(v,vstatus)
519 vstatus.cur_selection = ''
520 vstatus.time_to_autologout = None
521 vstatus.last_timeout_refresh = None
524 Triggered when the user has entered the id of something they would like to purchase.
526 def make_selection(v, vstatus):
527 # should use sudo here
528 if vstatus.cur_selection == '55':
529 vstatus.mk.set_message('OPENSESAME')
530 logging.info('dispensing a door for %s'%vstatus.username)
532 ret = os.system('dispense -u "%s" door'%vstatus.username)
534 ret = os.system('dispense door')
536 logging.info('door opened')
537 vstatus.mk.set_message(center('DOOR OPEN'))
539 logging.warning('user %s tried to dispense a bad door'%vstatus.username)
540 vstatus.mk.set_message(center('BAD DOOR'))
542 elif vstatus.cur_selection == '81':
544 elif vstatus.cur_selection == '99':
545 scroll_options(vstatus.username, vstatus.mk)
546 vstatus.cur_selection = ''
548 elif vstatus.cur_selection[1] == '8':
549 v.display('GOT DRINK?')
550 if ((os.system('dispense -u "%s" coke:%s'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
551 v.display('SEEMS NOT')
553 v.display('GOT DRINK!')
555 # first see if it's a named slot
557 price, shortname, name = get_snack( vstatus.cur_selection )
559 price, shortname, name = get_snack( '--' )
560 dollarprice = "$%.2f" % ( price / 100.0 )
561 v.display(vstatus.cur_selection+' - %s'%dollarprice)
562 exitcode = os.system('dispense -u "%s" snack:%s'%(vstatus.username, vstatus.cur_selection)) >> 8
564 # magic dispense syslog service
565 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
566 (worked, code, string) = v.vend(vstatus.cur_selection)
568 v.display('THANK YOU')
570 print "Vend Failed:", code, string
571 v.display('VEND FAIL')
572 elif (exitcode == 5): # RV_BALANCE
573 v.display('NO MONEY?')
574 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
575 v.display('EMPTY SLOT')
576 elif (exitcode == 1): # RV_BADITEM (Dead slot)
577 v.display('EMPTY SLOT')
579 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
580 v.display('UNK ERROR')
584 Find the price of an item.
586 def price_check(v, vstatus):
587 if vstatus.cur_selection[1] == '8':
588 args = ('dispense', 'iteminfo', 'coke:' + 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( vstatus.cur_selection )
596 price, shortname, name = get_snack( '--' )
597 dollarprice = "$%.2f" % ( price / 100.0 )
598 v.display(vstatus.cur_selection+' - %s'%dollarprice)
601 Triggered when the user presses a button while entering their pin.
603 def handle_getting_pin_key(state, event, params, v, vstatus):
604 #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
606 if len(vstatus.cur_pin) < PIN_LENGTH:
608 if vstatus.cur_pin == '':
609 vstatus.cur_user = ''
610 reset_idler(v, vstatus)
614 vstatus.mk.set_message('PIN: ')
616 vstatus.cur_pin += chr(key + ord('0'))
617 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
618 if len(vstatus.cur_pin) == PIN_LENGTH:
619 vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
622 vstatus.cur_selection = ''
623 vstatus.change_state(STATE_GET_SELECTION)
624 scroll_options(vstatus.username, vstatus.mk, True)
628 vstatus.mk.set_messages(
629 [(center('BAD PIN'), False, 1.0),
630 (center('SORRY'), False, 0.5)])
631 vstatus.cur_user = ''
634 reset_idler(v, vstatus, 2)
639 Triggered when the user presses a button while entering their user id.
641 def handle_getting_uid_key(state, event, params, v, vstatus):
642 #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
645 # complicated key handling here:
647 if len(vstatus.cur_user) == 0 and key == 9:
648 vstatus.cur_selection = ''
649 vstatus.time_to_autologout = None
650 vstatus.mk.set_message('PRICECHECK')
652 scroll_options('', vstatus.mk)
653 vstatus.change_state(STATE_GET_SELECTION)
656 if len(vstatus.cur_user) <8:
658 vstatus.cur_user = ''
660 reset_idler(v, vstatus)
662 vstatus.cur_user += chr(key + ord('0'))
663 #logging.info('dob: '+vstatus.cur_user)
664 if len(vstatus.cur_user) > 5:
665 vstatus.mk.set_message('>'+vstatus.cur_user)
667 vstatus.mk.set_message('UID: '+vstatus.cur_user)
669 if len(vstatus.cur_user) == 5:
670 uid = int(vstatus.cur_user)
673 logging.info('user '+vstatus.cur_user+' has a bad PIN')
679 Welcome to Picklevision Sytems, Sunnyvale, CA
681 Greetings Professor Falken.
686 Shall we play a game?
689 Please choose from the following menu:
696 6. Toxic and Biochemical Warfare
697 7. Global Thermonuclear War
701 Wouldn't you prefer a nice game of chess?
703 """.replace('\n',' ')
704 vstatus.mk.set_messages([(pfalken, False, 10)])
705 vstatus.cur_user = ''
708 reset_idler(v, vstatus, 10)
712 if not has_good_pin(uid):
713 logging.info('user '+vstatus.cur_user+' has a bad PIN')
714 vstatus.mk.set_messages(
715 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
716 vstatus.cur_user = ''
719 reset_idler(v, vstatus, 3)
723 if acct_is_disabled():
724 logging.info('user '+vstatus.cur_user+' is disabled')
725 vstatus.mk.set_messages(
726 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
727 vstatus.cur_user = ''
730 reset_idler(v, vstatus, 3)
735 vstatus.mk.set_message('PIN: ')
736 logging.info('need pin for user %s'%vstatus.cur_user)
737 vstatus.change_state(STATE_GETTING_PIN)
741 Triggered when a key is pressed and the machine is idling.
743 def handle_idle_key(state, event, params, v, vstatus):
744 #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
749 vstatus.cur_user = ''
750 reset_idler(v, vstatus)
753 vstatus.change_state(STATE_GETTING_UID)
754 run_handler(event, key, v, vstatus)
757 What to do when there is nothing to do.
759 def handle_idle_tick(state, event, params, v, vstatus):
761 if vstatus.mk.done():
764 if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
765 vstatus.time_of_next_idler = time() + 30
770 vstatus.mk.update_display()
772 vstatus.change_state(STATE_GRANDFATHER_CLOCK)
773 run_handler(event, params, v, vstatus)
777 Manages the beeps for the grandfather clock
779 def beep_on(when, before=0):
780 start = int(when - before)
784 if now >= start and now <= end:
788 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
789 ### check for interesting times
792 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
793 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
794 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
795 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
797 hourfromnow = localtime(time() + 3600)
799 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
800 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
801 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
803 ## check for X seconds to the hour
804 ## if case, update counter to 2
805 if beep_on(onthehour,15) \
806 or beep_on(halfhour,0) \
807 or beep_on(quarterhour,0) \
808 or beep_on(threequarterhour,0) \
809 or beep_on(fivetothehour,0):
810 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
811 run_handler(event, params, v, vstatus)
813 vstatus.change_state(STATE_IDLE)
815 def handle_grandfather_tick(state, event, params, v, vstatus):
819 ### we live in interesting times
822 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
823 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
824 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
825 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
827 hourfromnow = localtime(time() + 3600)
829 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
830 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
831 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
834 #print "when it fashionable to wear a onion on your hip"
836 if beep_on(onthehour,15):
838 next_hour=((hourfromnow[3] + 11) % 12) + 1
839 if onthehour - time() < next_hour and onthehour - time() > 0:
844 msg.append(("DING!", False, None))
846 msg.append((" DING!", False, None))
847 elif int(onthehour - time()) == 0:
849 msg.append((" BONG!", False, None))
850 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
851 elif beep_on(halfhour,0):
854 msg.append((" HALFHOUR ", False, 50))
855 elif beep_on(quarterhour,0):
858 msg.append((" QTR HOUR ", False, 50))
859 elif beep_on(threequarterhour,0):
862 msg.append((" 3 QTR HR ", False, 50))
863 elif beep_on(fivetothehour,0):
866 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
870 ## check for X seconds to the hour
873 vstatus.mk.set_messages(msg)
876 vstatus.mk.update_display()
877 ## if no longer case, return to idle
879 ## change idler to be clock
880 if go_idle and vstatus.mk.done():
881 vstatus.change_state(STATE_IDLE,1)
884 What to do when the door is open.
886 def handle_door_idle(state, event, params, v, vstatus):
887 def twiddle(clock,v,wise = 2):
889 v.display("-FEED ME-")
890 elif (clock % 4 == 1+wise):
891 v.display("\\FEED ME/")
892 elif (clock % 4 == 2):
893 v.display("-FEED ME-")
894 elif (clock % 4 == 3-wise):
895 v.display("/FEED ME\\")
897 # don't care right now.
900 if ((now % 60 % 2) == 0):
903 twiddle(now, v, wise=0)
906 What to do when the door is opened or closed.
908 def handle_door_event(state, event, params, v, vstatus):
909 if params == 0: #door open
910 vstatus.change_state(STATE_DOOR_OPENING)
911 logging.warning("Entering open door mode")
912 v.display("-FEED ME-")
914 vstatus.cur_user = ''
916 elif params == 1: #door closed
917 vstatus.change_state(STATE_DOOR_CLOSING)
918 reset_idler(v, vstatus, 3)
920 logging.warning('Leaving open door mode')
921 v.display("-YUM YUM!-")
924 Triggered when a user swipes their caed, and the machine is logged out.
926 def handle_mifare_event(state, event, params, v, vstatus):
929 # Translate card_id into uid.
930 if card_id == None or card_id == _last_card_id:
933 _last_card_id = card_id
936 vstatus.cur_user = get_uid(card_id)
937 logging.info('Mapped card id to uid %s'%vstatus.cur_user)
938 vstatus.username = get_uname(vstatus.cur_user)
939 if acct_is_disabled(vstatus.username):
940 vstatus.username = '-disabled-'
942 vstatus.username = None
943 if vstatus.username == '-disabled-':
945 vstatus.mk.set_messages(
946 [(center('ACCT DISABLED'), False, 1.0),
947 (center('SORRY'), False, 0.5)])
948 vstatus.cur_user = ''
950 vstatus.username = None
952 reset_idler(v, vstatus, 2)
954 elif vstatus.username:
956 vstatus.cur_selection = ''
957 vstatus.change_state(STATE_GET_SELECTION)
958 scroll_options(vstatus.username, vstatus.mk, True)
962 vstatus.mk.set_messages(
963 [(center('BAD CARD'), False, 1.0),
964 (center('SORRY'), False, 0.5)])
965 vstatus.cur_user = ''
969 reset_idler(v, vstatus, 2)
973 Triggered when a user swipes their card and the machine is logged in.
975 def handle_mifare_add_user_event(state, event, params, v, vstatus):
979 # Translate card_id into uid.
980 if card_id == None or card_id == _last_card_id:
983 _last_card_id = card_id
986 if get_uid(card_id) != None:
987 vstatus.mk.set_messages(
988 [(center('ALREADY'), False, 0.5),
989 (center('ENROLLED'), False, 0.5)])
991 # scroll_options(vstatus.username, vstatus.mk)
996 logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
997 set_card_id(vstatus.cur_user, card_id)
998 vstatus.mk.set_messages(
999 [(center('CARD'), False, 0.5),
1000 (center('ENROLLED'), False, 0.5)])
1002 # scroll_options(vstatus.username, vstatus.mk)
1004 def return_to_idle(state,event,params,v,vstatus):
1005 reset_idler(v, vstatus)
1008 Maps what to do when the state changes.
1010 def create_state_table(vstatus):
1011 vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
1012 vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
1013 vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
1014 vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
1016 vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
1017 vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
1018 vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
1019 vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
1021 vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
1022 vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
1023 vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
1024 vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
1026 vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
1027 vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = handle_door_event
1028 vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
1029 vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
1031 vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
1032 vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = handle_door_event
1033 vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
1034 vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
1036 vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
1037 vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = handle_door_event
1038 vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
1039 vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
1041 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
1042 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
1043 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = handle_door_event
1044 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = handle_door_event
1045 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
1046 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
1047 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
1050 Get what to do on a state change.
1052 def get_state_table_handler(vstatus, state, event, counter):
1053 return vstatus.state_table[(state,event,counter)]
1055 def time_to_next_update(vstatus):
1056 idle_update = vstatus.time_of_next_idlestep - time()
1057 if not vstatus.mk.done() and vstatus.mk.next_update is not None:
1058 mk_update = vstatus.mk.next_update - time()
1059 if mk_update < idle_update:
1060 idle_update = mk_update
1063 def run_forever(rfh, wfh, options, cf):
1064 v = VendingMachine(rfh, wfh, USE_MIFARE)
1065 vstatus = VendState(v)
1066 create_state_table(vstatus)
1068 logging.debug('PING is ' + str(v.ping()))
1070 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
1073 reset_idler(v, vstatus)
1079 except DispenseDatabaseException, e:
1080 logging.error('Database error: '+str(e))
1082 timeout = time_to_next_update(vstatus)
1083 e = v.next_event(timeout)
1086 run_handler(event, params, v, vstatus)
1088 def run_handler(event, params, v, vstatus):
1089 handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
1091 handler(vstatus.state, event, params, v, vstatus)
1094 Connect to the machine.
1096 def connect_to_vend(options, cf):
1099 logging.info('Connecting to vending machine using LAT')
1100 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
1101 rfh, wfh = latclient.get_fh()
1102 elif options.use_serial:
1103 # Open vending machine via serial.
1104 logging.info('Connecting to vending machine using serial')
1105 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
1106 rfh,wfh = serialclient.get_fh()
1108 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
1109 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
1111 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
1112 sock.connect((options.host, options.port))
1113 rfh = sock.makefile('r')
1114 wfh = sock.makefile('w')
1121 Parse arguments from the command line
1124 from optparse import OptionParser
1126 op = OptionParser(usage="%prog [OPTION]...")
1127 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')
1128 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
1129 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
1130 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
1131 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
1132 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
1133 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
1134 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
1135 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
1136 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
1137 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
1138 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
1139 options, args = op.parse_args()
1142 op.error('extra command line arguments: ' + ' '.join(args))
1146 def create_pid_file(name):
1148 pid_file = file(name, 'w')
1149 pid_file.write('%d\n'%os.getpid())
1152 logging.warning('unable to write to pid file '+name+': '+str(e))
1155 def do_nothing(signum, stack):
1156 signal.signal(signum, do_nothing)
1157 def stop_server(signum, stack): raise KeyboardInterrupt
1158 signal.signal(signal.SIGHUP, do_nothing)
1159 signal.signal(signal.SIGTERM, stop_server)
1160 signal.signal(signal.SIGINT, stop_server)
1162 options = parse_args()
1163 config_opts = VendConfigFile(options.config_file, config_options)
1164 if options.daemon: become_daemon()
1165 set_up_logging(options)
1166 if options.pid_file != '': create_pid_file(options.pid_file)
1168 return options, config_opts
1170 def clean_up_nicely(options, config_opts):
1171 if options.pid_file != '':
1173 os.unlink(options.pid_file)
1174 logging.debug('Removed pid file '+options.pid_file)
1175 except OSError: pass # if we can't delete it, meh
1177 def set_up_logging(options):
1178 logger = logging.getLogger()
1180 if not options.daemon:
1181 stderr_logger = logging.StreamHandler(sys.stderr)
1182 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1183 logger.addHandler(stderr_logger)
1185 if options.log_file != '':
1187 file_logger = logging.FileHandler(options.log_file)
1188 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1189 logger.addHandler(file_logger)
1191 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1193 if options.syslog != None:
1194 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1195 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1196 logger.addHandler(sys_logger)
1199 logger.setLevel(logging.WARNING)
1200 elif options.verbose:
1201 logger.setLevel(logging.DEBUG)
1203 logger.setLevel(logging.INFO)
1205 def become_daemon():
1206 dev_null = file('/dev/null')
1207 fd = dev_null.fileno()
1216 raise SystemExit('failed to fork: '+str(e))
1218 def do_vend_server(options, config_opts):
1221 rfh, wfh = connect_to_vend(options, config_opts)
1222 except (SerialClientException, socket.error), e:
1223 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1225 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1226 logging.info("Trying again in 5 seconds.")
1230 # run_forever(rfh, wfh, options, config_opts)
1233 run_forever(rfh, wfh, options, config_opts)
1234 except VendingException:
1235 logging.error("Connection died, trying again...")
1236 logging.info("Trying again in 5 seconds.")
1240 def main(argv=None):
1241 options, config_opts = set_stuff_up()
1244 logging.warning('Starting Vend Server')
1245 do_vend_server(options, config_opts)
1246 logging.error('Vend Server finished unexpectedly, restarting')
1247 except KeyboardInterrupt:
1248 logging.info("Killed by signal, cleaning up")
1249 clean_up_nicely(options, config_opts)
1250 logging.warning("Vend Server stopped")
1255 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1256 tb = format_tb(exc_traceback, 20)
1259 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1260 logging.critical("Message: " + str(exc_value))
1261 logging.critical("Traceback:")
1263 for line in event.split('\n'):
1264 logging.critical(' '+line)
1265 logging.critical("This message should be considered a bug in the Vend Server.")
1266 logging.critical("Please report this to someone who can fix it.")
1268 logging.warning("Trying again anyway (might not help, but hey...)")
1270 if __name__ == '__main__':