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
205 In here just for fun.
209 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
210 choice = int(random()*len(messages))
211 msg = messages[choice]
212 left = range(len(msg))
213 for i in range(len(msg)):
214 if msg[i] == ' ': left.remove(i)
218 for i in range(0, len(msg)):
224 s += chr(int(random()*26)+ord('A'))
232 Format text so it will appear centered on the screen.
234 def center(self, str):
236 return ' '*((LEN-len(str))/2)+str
239 Configure the things that will appear on screen whil the machine is idling.
241 def setup_idlers(self):
245 GrayIdler(self.v,one="*",zero="-"),
246 GrayIdler(self.v,one="/",zero="\\"),
247 GrayIdler(self.v,one="X",zero="O"),
248 GrayIdler(self.v,one="*",zero="-",reorder=1),
249 GrayIdler(self.v,one="/",zero="\\",reorder=1),
250 GrayIdler(self.v,one="X",zero="O",reorder=1),
256 StringIdler(self.v), # Hello Cruel World
257 StringIdler(self.v, text="Kill 'em all", repeat=False),
258 StringIdler(self.v, text=CREDITS),
259 StringIdler(self.v, text=str(math.pi) + " "),
260 StringIdler(self.v, text=str(math.e) + " "),
261 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),
262 # "Hello World" in brainfuck
263 StringIdler(self.v, text=">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++."),
267 FileIdler(self.v, '/usr/share/common-licenses/GPL-2',affinity=2),
269 PipeIdler(self.v, "/usr/bin/getent", "passwd"),
270 FortuneIdler(self.v,affinity=20),
276 Go back to the default idler.
278 def reset_idler(self, t = None):
279 self.idler = GreetingIdler(self.v, t)
280 self.vstatus.time_of_next_idlestep = time()+self.idler.next()
281 self.vstatus.time_of_next_idler = None
282 self.vstatus.time_to_autologout = None
283 self.vstatus.change_state(STATE_IDLE, 1)
286 Change to a random idler.
288 def choose_idler(self):
290 # Implementation of the King Of the Hill algorithm from;
291 # http://eli.thegreenplace.net/2010/01/22/weighted-random-generation-in-python/
293 #def weighted_choice_king(weights):
296 # for i, w in enumerate(weights):
298 # if random.random() * total < w:
306 for choice in self.idlers:
307 weight = choice.affinity()
309 if random() * total < weight:
317 Run every step while the machine is idling.
320 if self.idler.finished():
322 self.vstatus.time_of_next_idler = time() + 30
323 nextidle = self.idler.next()
325 nextidle = IDLE_SPEED
326 self.vstatus.time_of_next_idlestep = time()+nextidle
329 These next two events trigger no response in the code.
331 def handle_tick_event(self, event, params):
332 # don't care right now.
335 def handle_switch_event(self, event, params):
336 # don't care right now.
340 Don't do anything for this event.
342 def do_nothing(self, event, params):
343 print "doing nothing (s,e,p)", state, " ", event, " ", params
347 These next few entrie tell us to do nothing while we are idling
349 def handle_getting_uid_idle(self, event, params):
350 # don't care right now.
353 def handle_getting_pin_idle(self, event, params):
354 # don't care right now.
358 While logged in and waiting for user input, slowly get closer to logging out.
360 def handle_get_selection_idle(self, event, params):
361 # don't care right now.
363 ### State logging out ..
364 if self.vstatus.time_to_autologout != None:
365 time_left = self.vstatus.time_to_autologout - time()
366 if time_left < 6 and (self.vstatus.last_timeout_refresh is None or self.vstatus.last_timeout_refresh > time_left):
367 self.vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
368 self.vstatus.last_timeout_refresh = int(time_left)
369 self.vstatus.cur_selection = ''
371 if self.vstatus.time_to_autologout != None and self.vstatus.time_to_autologout - time() <= 0:
372 self.vstatus.time_to_autologout = None
373 self.vstatus.cur_user = ''
374 self.vstatus.cur_pin = ''
375 self.vstatus.cur_selection = ''
376 self._last_card_id = -1
379 ### State fully logged out ... reset variables
380 if self.vstatus.time_to_autologout and not self.vstatus.mk.done():
381 self.vstatus.time_to_autologout = None
382 if self.vstatus.cur_user == '' and self.vstatus.time_to_autologout:
383 self.vstatus.time_to_autologout = None
386 if len(self.vstatus.cur_pin) == PIN_LENGTH and self.vstatus.mk.done() and self.vstatus.time_to_autologout == None:
388 self.vstatus.time_to_autologout = time() + 15
389 self.vstatus.last_timeout_refresh = None
391 ## FIXME - this may need to be elsewhere.....
393 self.vstatus.mk.update_display()
396 Triggered on user input while logged in.
398 def handle_get_selection_key(self, event, params):
400 if len(self.vstatus.cur_selection) == 0:
402 self.vstatus.cur_pin = ''
403 self.vstatus.cur_user = ''
404 self.vstatus.cur_selection = ''
406 self.vstatus.mk.set_messages([(self.center('BYE!'), False, 1.5)])
409 self.vstatus.cur_selection += chr(key + ord('0'))
410 self.vstatus.mk.set_message('SELECT: '+self.vstatus.cur_selection)
411 self.vstatus.time_to_autologout = None
412 elif len(self.vstatus.cur_selection) == 1:
414 self.vstatus.cur_selection = ''
415 self.vstatus.time_to_autologout = None
416 self.scroll_options(self.vstatus.username, self.vstatus.mk)
419 self.vstatus.cur_selection += chr(key + ord('0'))
420 if self.vstatus.cur_user:
421 self.make_selection()
422 self.vstatus.cur_selection = ''
423 self.vstatus.time_to_autologout = time() + 8
424 self.vstatus.last_timeout_refresh = None
427 self.dispense.getItemInfo(self.vstatus.cur_selection)
428 self.vstatus.cur_selection = ''
429 self.vstatus.time_to_autologout = None
430 self.vstatus.last_timeout_refresh = None
433 Triggered when the user has entered the id of something they would like to purchase.
435 def make_selection(self):
436 # should use sudo here
437 if self.vstatus.cur_selection == '55':
438 self.vstatus.mk.set_message('OPENSESAME')
439 logging.info('dispensing a door for %s'%self.vstatus.username)
441 ret = os.system('dispense -u "%s" door'%self.vstatus.username)
443 ret = os.system('dispense door')
445 logging.info('door opened')
446 self.vstatus.mk.set_message(self.center('DOOR OPEN'))
448 logging.warning('user %s tried to dispense a bad door'%self.vstatus.username)
449 self.vstatus.mk.set_message(self.center('BAD DOOR'))
451 elif self.vstatus.cur_selection == '81':
453 elif self.vstatus.cur_selection == '99':
454 self.scroll_options(self.vstatus.username, self.vstatus.mk)
455 self.vstatus.cur_selection = ''
457 elif self.vstatus.cur_selection[1] == '8':
458 self.v.display('GOT DRINK?')
459 if ((os.system('dispense -u "%s" coke:%s'%(self.vstatus.username, self.vstatus.cur_selection[0])) >> 8) != 0):
460 self.v.display('SEEMS NOT')
462 self.v.display('GOT DRINK!')
464 # first see if it's a named slot
466 price, shortname, name = get_snack( self.vstatus.cur_selection )
468 price, shortname, name = get_snack( '--' )
469 dollarprice = "$%.2f" % ( price / 100.0 )
470 self.v.display(self.vstatus.cur_selection+' - %s'%dollarprice)
471 exitcode = os.system('dispense -u "%s" snack:%s'%(self.vstatus.username, self.vstatus.cur_selection)) >> 8
473 # magic dispense syslog service
474 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, self.vstatus.cur_selection, self.vstatus.username))
475 (worked, code, string) = self.v.vend(self.vstatus.cur_selection)
477 self.v.display('THANK YOU')
479 print "Vend Failed:", code, string
480 self.v.display('VEND FAIL')
481 elif (exitcode == 5): # RV_BALANCE
482 self.v.display('NO MONEY?')
483 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
484 self.v.display('EMPTY SLOT')
485 elif (exitcode == 1): # RV_BADITEM (Dead slot)
486 self.v.display('EMPTY SLOT')
488 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))
489 self.v.display('UNK ERROR')
493 Triggered when the user presses a button while entering their pin.
495 def handle_getting_pin_key(self, event, params):
497 if len(self.vstatus.cur_pin) < PIN_LENGTH:
499 if self.vstatus.cur_pin == '':
500 self.vstatus.cur_user = ''
504 self.vstatus.cur_pin = ''
505 self.vstatus.mk.set_message('PIN: ')
507 self.vstatus.cur_pin += chr(key + ord('0'))
508 self.vstatus.mk.set_message('PIN: '+'X'*len(self.vstatus.cur_pin))
509 if len(self.vstatus.cur_pin) == PIN_LENGTH:
510 self.dispense.authUserIdPin(self.vstatus.cur_user, self.vstatus.cur_pin)
511 if self.dispense.getUsername():
512 self.v.beep(0, False)
513 self.vstatus.cur_selection = ''
514 self.vstatus.change_state(STATE_GET_SELECTION)
515 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
518 self.v.beep(40, False)
519 self.vstatus.mk.set_messages(
520 [(self.center('BAD PIN'), False, 1.0),
521 (self.center('SORRY'), False, 0.5)])
522 self.vstatus.cur_user = ''
523 self.vstatus.cur_pin = ''
530 Triggered when the user presses a button while entering their user id.
532 def handle_getting_uid_key(self, event, params):
534 # complicated key handling here:
536 if len(self.vstatus.cur_user) == 0 and key == 9:
537 self.vstatus.cur_selection = ''
538 self.vstatus.time_to_autologout = None
539 self.vstatus.mk.set_message('PRICECHECK')
541 self.scroll_options('', self.vstatus.mk)
542 self.vstatus.change_state(STATE_GET_SELECTION)
545 if len(self.vstatus.cur_user) <8:
547 self.vstatus.cur_user = ''
551 self.vstatus.cur_user += chr(key + ord('0'))
552 #logging.info('dob: '+vstatus.cur_user)
553 if len(self.vstatus.cur_user) > 5:
554 self.vstatus.mk.set_message('>'+self.vstatus.cur_user)
556 self.vstatus.mk.set_message('UID: '+self.vstatus.cur_user)
558 if len(self.vstatus.cur_user) == 5:
559 uid = int(self.vstatus.cur_user)
562 logging.info('user '+self.vstatus.cur_user+' has a bad PIN')
568 Welcome to Picklevision Sytems, Sunnyvale, CA
570 Greetings Professor Falken.
575 Shall we play a game?
578 Please choose from the following menu:
585 6. Toxic and Biochemical Warfare
586 7. Global Thermonuclear War
590 Wouldn't you prefer a nice game of chess?
592 """.replace('\n',' ')
593 self.vstatus.mk.set_messages([(pfalken, False, 10)])
594 self.vstatus.cur_user = ''
595 self.vstatus.cur_pin = ''
601 # TODO Fix this up do we can check before logging in
603 if self.dispense.isDisabled():
604 logging.info('user '+self.vstatus.cur_user+' is disabled')
605 self.vstatus.mk.set_messages(
606 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
607 self.vstatus.cur_user = ''
608 self.vstatus.cur_pin = ''
614 self.vstatus.cur_pin = ''
615 self.vstatus.mk.set_message('PIN: ')
616 logging.info('need pin for user %s'%self.vstatus.cur_user)
617 self.vstatus.change_state(STATE_GETTING_PIN)
621 Triggered when a key is pressed and the machine is idling.
623 def handle_idle_key(self, event, params):
626 self.vstatus.cur_user = ''
630 self.vstatus.change_state(STATE_GETTING_UID)
631 self.run_handler(event, params)
634 What to do when there is nothing to do.
636 def handle_idle_tick(self, event, params):
638 if self.vstatus.mk.done():
641 if self.vstatus.time_of_next_idler and time() > self.vstatus.time_of_next_idler:
642 self.vstatus.time_of_next_idler = time() + 30
647 self.vstatus.mk.update_display()
649 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK)
650 self.run_handler(event, params)
654 Manages the beeps for the grandfather clock
656 def beep_on(self, when, before=0):
657 start = int(when - before)
661 if now >= start and now <= end:
665 def handle_idle_grandfather_tick(self, event, params):
666 ### check for interesting times
669 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
670 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
671 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
672 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
674 hourfromnow = localtime(time() + 3600)
676 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
677 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
678 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
680 ## check for X seconds to the hour
681 ## if case, update counter to 2
682 if self.beep_on(onthehour,15) \
683 or self.beep_on(halfhour,0) \
684 or self.beep_on(quarterhour,0) \
685 or self.beep_on(threequarterhour,0) \
686 or self.beep_on(fivetothehour,0):
687 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
688 self.run_handler(event, params)
690 self.vstatus.change_state(STATE_IDLE)
692 def handle_grandfather_tick(self, event, params):
696 ### we live in interesting times
699 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
700 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
701 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
702 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
704 hourfromnow = localtime(time() + 3600)
706 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
707 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
708 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
711 #print "when it fashionable to wear a onion on your hip"
713 if self.beep_on(onthehour,15):
715 next_hour=((hourfromnow[3] + 11) % 12) + 1
716 if onthehour - time() < next_hour and onthehour - time() > 0:
717 self.v.beep(0, False)
721 msg.append(("DING!", False, None))
723 msg.append((" DING!", False, None))
724 elif int(onthehour - time()) == 0:
725 self.v.beep(255, False)
726 msg.append((" BONG!", False, None))
727 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
728 elif self.beep_on(halfhour,0):
730 self.v.beep(0, False)
731 msg.append((" HALFHOUR ", False, 50))
732 elif self.beep_on(quarterhour,0):
734 self.v.beep(0, False)
735 msg.append((" QTR HOUR ", False, 50))
736 elif self.beep_on(threequarterhour,0):
738 self.v.beep(0, False)
739 msg.append((" 3 QTR HR ", False, 50))
740 elif self.beep_on(fivetothehour,0):
742 self.v.beep(0, False)
743 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
747 ## check for X seconds to the hour
750 self.vstatus.mk.set_messages(msg)
753 self.vstatus.mk.update_display()
754 ## if no longer case, return to idle
756 ## change idler to be clock
757 if go_idle and self.vstatus.mk.done():
758 self.vstatus.change_state(STATE_IDLE,1)
761 What to do when the door is open.
763 def handle_door_idle(self, event, params):
764 def twiddle(clock,v,wise = 2):
766 v.display("-FEED ME-")
767 elif (clock % 4 == 1+wise):
768 v.display("\\FEED ME/")
769 elif (clock % 4 == 2):
770 v.display("-FEED ME-")
771 elif (clock % 4 == 3-wise):
772 v.display("/FEED ME\\")
774 # don't care right now.
777 if ((now % 60 % 2) == 0):
780 twiddle(now, self.v, wise=0)
783 What to do when the door is opened or closed.
785 def handle_door_event(self, event, params):
786 if params == 0: #door open
787 self.vstatus.change_state(STATE_DOOR_OPENING)
788 logging.warning("Entering open door mode")
789 self.v.display("-FEED ME-")
791 self.vstatus.cur_user = ''
792 self.vstatus.cur_pin = ''
793 elif params == 1: #door closed
794 self.vstatus.change_state(STATE_DOOR_CLOSING)
797 logging.warning('Leaving open door mode')
798 self.v.display("-YUM YUM!-")
801 Triggered when a user swipes their caed, and the machine is logged out.
803 def handle_mifare_event(self, event, params):
805 # Translate card_id into uid.
806 if card_id == None or card_id == self._last_card_id:
809 self._last_card_id = card_id
811 self.dispense.authMifareCard(card_id)
812 logging.info('Mapped card id to uid %s'%self.dispense.getUsername())
813 if not self.dispense.isLoggedIn():
814 self.v.beep(40, False)
815 self.vstatus.mk.set_messages(
816 [(self.center('BAD CARD'), False, 1.0),
817 (self.center('SORRY'), False, 0.5)])
818 self.vstatus.cur_user = ''
819 self.vstatus.cur_pin = ''
820 self._last_card_id = -1
824 elif self.dispense.isDisabled():
825 self.v.beep(40, False)
826 self.vstatus.mk.set_messages(
827 [(self.center('ACCT DISABLED'), False, 1.0),
828 (self.center('SORRY'), False, 0.5)])
829 self.dispense.logOut()
833 self.vstatus.cur_selection = ''
834 self.vstatus.change_state(STATE_GET_SELECTION)
835 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
839 Triggered when a user swipes their card and the machine is logged in.
841 def handle_mifare_add_user_event(self, event, params):
844 # Translate card_id into uid.
845 if card_id == None or card_id == self._last_card_id:
848 self._last_card_id = card_id
850 res = self.dispense.addCard(card_id)
852 if get_uid(card_id) != None:
853 self.vstatus.mk.set_messages(
854 [(self.center('ALREADY'), False, 0.5),
855 (self.center('ENROLLED'), False, 0.5)])
857 logging.info('Enrolling card %s to uid %s (%s)'%(card_id, self.vstatus.cur_user, self.vstatus.username))
858 self.set_card_id(self.vstatus.cur_user, self.card_id)
859 self.vstatus.mk.set_messages(
860 [(self.center('CARD'), False, 0.5),
861 (self.center('ENROLLED'), False, 0.5)])
863 def return_to_idle(self, event, params):
867 Maps what to do when the state changes.
869 def create_state_table(self):
870 self.vstatus.state_table[(STATE_IDLE,TICK,1)] = self.handle_idle_tick
871 self.vstatus.state_table[(STATE_IDLE,KEY,1)] = self.handle_idle_key
872 self.vstatus.state_table[(STATE_IDLE,DOOR,1)] = self.handle_door_event
873 self.vstatus.state_table[(STATE_IDLE,MIFARE,1)] = self.handle_mifare_event
875 self.vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = self.handle_door_idle
876 self.vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = self.handle_door_event
877 self.vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = self.do_nothing
878 self.vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = self.do_nothing
880 self.vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = self.return_to_idle
881 self.vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = self.handle_door_event
882 self.vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = self.do_nothing
883 self.vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = self.do_nothing
885 self.vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = self.handle_getting_uid_idle
886 self.vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = self.handle_door_event
887 self.vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = self.handle_getting_uid_key
888 self.vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = self.handle_mifare_event
890 self.vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = self.handle_getting_pin_idle
891 self.vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = self.handle_door_event
892 self.vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = self.handle_getting_pin_key
893 self.vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = self.handle_mifare_event
895 self.vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = self.handle_get_selection_idle
896 self.vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = self.handle_door_event
897 self.vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = self.handle_get_selection_key
898 self.vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = self.handle_mifare_add_user_event
900 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = self.handle_idle_grandfather_tick
901 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = self.handle_grandfather_tick
902 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = self.handle_door_event
903 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = self.handle_door_event
904 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = self.do_nothing
905 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = self.do_nothing
906 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = self.handle_mifare_event
909 Get what to do on a state change.
911 def get_state_table_handler(self, state, event, counter):
912 return self.vstatus.state_table[(state,event,counter)]
914 def time_to_next_update(self):
915 idle_update = self.vstatus.time_of_next_idlestep - time()
916 if not self.vstatus.mk.done() and self.vstatus.mk.next_update is not None:
917 mk_update = self.vstatus.mk.next_update - time()
918 if mk_update < idle_update:
919 idle_update = mk_update
922 def run_forever(self, rfh, wfh, options, cf):
923 self.v = VendingMachine(rfh, wfh, USE_MIFARE)
924 self.dispense = Dispense()
925 self.vstatus = VendState(self.v)
926 self.create_state_table()
928 logging.debug('PING is ' + str(self.v.ping()))
930 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
939 except DispenseDatabaseException, e:
940 logging.error('Database error: '+str(e))
942 timeout = self.time_to_next_update()
943 (event, params) = self.v.next_event(timeout)
944 self.run_handler(event, params)
946 def run_handler(self, event, params):
947 handler = self.get_state_table_handler(self.vstatus.state,event,self.vstatus.counter)
949 handler(event, params)
952 Connect to the machine.
954 def connect_to_vend(options, cf):
957 logging.info('Connecting to vending machine using LAT')
958 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
959 rfh, wfh = latclient.get_fh()
960 elif options.use_serial:
961 # Open vending machine via serial.
962 logging.info('Connecting to vending machine using serial')
963 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
964 rfh,wfh = serialclient.get_fh()
966 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
967 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
969 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
970 sock.connect((options.host, options.port))
971 rfh = sock.makefile('r')
972 wfh = sock.makefile('w')
979 Parse arguments from the command line
982 from optparse import OptionParser
984 op = OptionParser(usage="%prog [OPTION]...")
985 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')
986 op.add_option('--serial', action='store_true', default=False, dest='use_serial', help='use the serial port')
987 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
988 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
989 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
990 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
991 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
992 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
993 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
994 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
995 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
996 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
997 options, args = op.parse_args()
1000 op.error('extra command line arguments: ' + ' '.join(args))
1004 def create_pid_file(name):
1006 pid_file = file(name, 'w')
1007 pid_file.write('%d\n'%os.getpid())
1010 logging.warning('unable to write to pid file '+name+': '+str(e))
1013 def do_nothing(signum, stack):
1014 signal.signal(signum, do_nothing)
1015 def stop_server(signum, stack): raise KeyboardInterrupt
1016 signal.signal(signal.SIGHUP, do_nothing)
1017 signal.signal(signal.SIGTERM, stop_server)
1018 signal.signal(signal.SIGINT, stop_server)
1020 options = parse_args()
1021 config_opts = VendConfigFile(options.config_file, config_options)
1022 if options.daemon: become_daemon()
1023 set_up_logging(options)
1024 if options.pid_file != '': create_pid_file(options.pid_file)
1026 return options, config_opts
1028 def clean_up_nicely(options, config_opts):
1029 if options.pid_file != '':
1031 os.unlink(options.pid_file)
1032 logging.debug('Removed pid file '+options.pid_file)
1033 except OSError: pass # if we can't delete it, meh
1035 def set_up_logging(options):
1036 logger = logging.getLogger()
1038 if not options.daemon:
1039 stderr_logger = logging.StreamHandler(sys.stderr)
1040 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1041 logger.addHandler(stderr_logger)
1043 if options.log_file != '':
1045 file_logger = logging.FileHandler(options.log_file)
1046 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1047 logger.addHandler(file_logger)
1049 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1051 if options.syslog != None:
1052 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1053 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1054 logger.addHandler(sys_logger)
1057 logger.setLevel(logging.WARNING)
1058 elif options.verbose:
1059 logger.setLevel(logging.DEBUG)
1061 logger.setLevel(logging.INFO)
1063 def become_daemon():
1064 dev_null = file('/dev/null')
1065 fd = dev_null.fileno()
1074 raise SystemExit('failed to fork: '+str(e))
1076 def do_vend_server(options, config_opts):
1079 rfh, wfh = connect_to_vend(options, config_opts)
1080 except (SerialClientException, socket.error), e:
1081 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1083 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1084 logging.info("Trying again in 5 seconds.")
1088 # run_forever(rfh, wfh, options, config_opts)
1091 vserver = VendServer()
1092 vserver.run_forever(rfh, wfh, options, config_opts)
1093 except VendingException:
1094 logging.error("Connection died, trying again...")
1095 logging.info("Trying again in 5 seconds.")
1099 def main(argv=None):
1100 options, config_opts = set_stuff_up()
1103 logging.warning('Starting Vend Server')
1104 do_vend_server(options, config_opts)
1105 logging.error('Vend Server finished unexpectedly, restarting')
1106 except KeyboardInterrupt:
1107 logging.info("Killed by signal, cleaning up")
1108 clean_up_nicely(options, config_opts)
1109 logging.warning("Vend Server stopped")
1114 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1115 tb = format_tb(exc_traceback, 20)
1118 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1119 logging.critical("Message: " + str(exc_value))
1120 logging.critical("Traceback:")
1122 for line in event.split('\n'):
1123 logging.critical(' '+line)
1124 logging.critical("This message should be considered a bug in the Vend Server.")
1125 logging.critical("Please report this to someone who can fix it.")
1127 logging.warning("Trying again anyway (might not help, but hey...)")
1129 if __name__ == '__main__':