2 # vim: ts=4 sts=4 sw=4 noexpandtab
7 import sys, os, string, re, pwd, signal, math, syslog
8 import logging, logging.handlers
9 from traceback import format_tb
10 from time import time, sleep, mktime, localtime
11 from subprocess import Popen, PIPE
12 from LATClient import LATClient, LATClientException
13 from SerialClient import SerialClient, SerialClientException
14 from VendingMachine import VendingMachine, VendingException
15 from MessageKeeper import MessageKeeper
16 from HorizScroll import HorizScroll
17 from random import random, seed
18 from Idler import GreetingIdler,TrainIdler,GrayIdler,StringIdler,ClockIdler,FortuneIdler,FileIdler,PipeIdler
19 from SnackConfig import get_snack#, get_snacks
21 from posix import geteuid
22 from OpenDispense import OpenDispense as Dispense
25 This vending machine software brought to you by:
30 and a collective of hungry alpacas.
32 The MIFARE card reader bought to you by:
35 Bug Hunting and hardware maintenance by:
38 For a good time call +61 8 6488 3901
60 STATE_GRANDFATHER_CLOCK,
68 'DBServer': ('Database', 'Server'),
69 'DBName': ('Database', 'Name'),
70 'DBUser': ('VendingMachine', 'DBUser'),
71 'DBPassword': ('VendingMachine', 'DBPassword'),
73 'ServiceName': ('VendingMachine', 'ServiceName'),
74 'ServicePassword': ('VendingMachine', 'Password'),
76 'ServerName': ('DecServer', 'Name'),
77 'ConnectPassword': ('DecServer', 'ConnectPassword'),
78 'PrivPassword': ('DecServer', 'PrivPassword'),
82 def __init__(self, config_file, options):
84 cp = ConfigParser.ConfigParser()
87 for option in options:
88 section, name = options[option]
89 value = cp.get(section, name)
90 self.__dict__[option] = value
92 except ConfigParser.Error, e:
93 raise SystemExit("Error reading config file "+config_file+": " + str(e))
96 This class manages the current state of the vending machine.
100 self.state_table = {}
101 self.state = STATE_IDLE
104 self.mk = MessageKeeper(v)
108 self.cur_selection = ''
109 self.time_to_autologout = None
111 self.last_timeout_refresh = None
113 def change_state(self,newstate,newcounter=None):
114 if self.state != newstate:
115 self.state = newstate
117 if newcounter is not None and self.counter != newcounter:
118 self.counter = newcounter
137 Show information to the user as to what can be dispensed.
139 def scroll_options(self, username, mk, welcome = False):
140 # If the user has just logged in, show them their balance
142 balance = self.dispense.getBalance()
143 balance = balance[:-4] + '.' + balance[-4] + balance[-2:] # Work around display bug
145 msg = [(self.center('WELCOME'), False, TEXT_SPEED),
146 (self.center(self.dispense.getUsername()), False, TEXT_SPEED),
147 (self.center(balance), False, TEXT_SPEED),]
150 choices = ' '*10+'CHOICES: '
152 # Show what is in the coke machine
153 # Need to update this so it uses the abstracted system
155 for i in ['08', '18', '28', '38', '48', '58', '68']:
156 cokes.append((i, self.dispense.getItemInfo(i)))
159 if c[1][0] == 'dead':
161 choices += '%s-(%sc)-%s8 '%(c[1][0], c[1][1], c[0])
163 # Show the final few options
164 choices += '55-DOOR '
165 choices += 'OR ANOTHER SNACK. '
166 choices += '99 TO READ AGAIN. '
167 choices += 'CHOICE? '
168 msg.append((choices, False, None))
169 # Send it to the display
173 In here just for fun.
177 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
178 choice = int(random()*len(messages))
179 msg = messages[choice]
180 left = range(len(msg))
181 for i in range(len(msg)):
182 if msg[i] == ' ': left.remove(i)
186 for i in range(0, len(msg)):
192 s += chr(int(random()*26)+ord('A'))
200 Format text so it will appear centered on the screen.
202 def center(self, str):
204 return ' '*((LEN-len(str))/2)+str
207 Configure the things that will appear on screen whil the machine is idling.
209 def setup_idlers(self):
213 GrayIdler(self.v,one="*",zero="-"),
214 GrayIdler(self.v,one="/",zero="\\"),
215 GrayIdler(self.v,one="X",zero="O"),
216 GrayIdler(self.v,one="*",zero="-",reorder=1),
217 GrayIdler(self.v,one="/",zero="\\",reorder=1),
218 GrayIdler(self.v,one="X",zero="O",reorder=1),
224 StringIdler(self.v), # Hello Cruel World
225 StringIdler(self.v, text="Kill 'em all", repeat=False),
226 StringIdler(self.v, text=CREDITS),
227 StringIdler(self.v, text=str(math.pi) + " "),
228 StringIdler(self.v, text=str(math.e) + " "),
229 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),
230 # "Hello World" in brainfuck
231 StringIdler(self.v, text=">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++."),
235 FileIdler(self.v, '/usr/share/common-licenses/GPL-2',affinity=2),
237 PipeIdler(self.v, "/usr/bin/getent", "passwd"),
238 FortuneIdler(self.v,affinity=20),
244 Go back to the default idler.
246 def reset_idler(self, t = None):
247 self.idler = GreetingIdler(self.v, t)
248 self.vstatus.time_of_next_idlestep = time()+self.idler.next()
249 self.vstatus.time_of_next_idler = None
250 self.vstatus.time_to_autologout = None
251 self.vstatus.change_state(STATE_IDLE, 1)
254 Change to a random idler.
256 def choose_idler(self):
258 # Implementation of the King Of the Hill algorithm from;
259 # http://eli.thegreenplace.net/2010/01/22/weighted-random-generation-in-python/
261 #def weighted_choice_king(weights):
264 # for i, w in enumerate(weights):
266 # if random.random() * total < w:
274 for choice in self.idlers:
275 weight = choice.affinity()
277 if random() * total < weight:
285 Run every step while the machine is idling.
288 if self.idler.finished():
290 self.vstatus.time_of_next_idler = time() + 30
291 nextidle = self.idler.next()
293 nextidle = IDLE_SPEED
294 self.vstatus.time_of_next_idlestep = time()+nextidle
297 These next two events trigger no response in the code.
299 def handle_tick_event(self, event, params):
300 # don't care right now.
303 def handle_switch_event(self, event, params):
304 # don't care right now.
308 Don't do anything for this event.
310 def do_nothing(self, event, params):
311 print "doing nothing (s,e,p)", state, " ", event, " ", params
315 These next few entrie tell us to do nothing while we are idling
317 def handle_getting_uid_idle(self, event, params):
318 # don't care right now.
321 def handle_getting_pin_idle(self, event, params):
322 # don't care right now.
326 While logged in and waiting for user input, slowly get closer to logging out.
328 def handle_get_selection_idle(self, event, params):
329 # don't care right now.
331 ### State logging out ..
332 if self.vstatus.time_to_autologout != None:
333 time_left = self.vstatus.time_to_autologout - time()
334 if time_left < 6 and (self.vstatus.last_timeout_refresh is None or self.vstatus.last_timeout_refresh > time_left):
335 self.vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
336 self.vstatus.last_timeout_refresh = int(time_left)
337 self.vstatus.cur_selection = ''
339 # Login timed out: Log out the current user.
340 if self.vstatus.time_to_autologout != None and self.vstatus.time_to_autologout - time() <= 0:
341 self.vstatus.time_to_autologout = None
342 self.vstatus.cur_user = ''
343 self.vstatus.cur_pin = ''
344 self.vstatus.cur_selection = ''
345 self._last_card_id = -1
346 self.dispense.logOut()
349 ### State fully logged out ... reset variables
350 if self.vstatus.time_to_autologout and not self.vstatus.mk.done():
351 self.vstatus.time_to_autologout = None
352 if self.vstatus.cur_user == '' and self.vstatus.time_to_autologout:
353 self.vstatus.time_to_autologout = None
356 if len(self.vstatus.cur_pin) == PIN_LENGTH and self.vstatus.mk.done() and self.vstatus.time_to_autologout == None:
358 self.vstatus.time_to_autologout = time() + 15
359 self.vstatus.last_timeout_refresh = None
361 ## FIXME - this may need to be elsewhere.....
363 self.vstatus.mk.update_display()
366 Triggered on user input while logged in.
368 def handle_get_selection_key(self, event, params):
370 if len(self.vstatus.cur_selection) == 0:
372 self.vstatus.cur_pin = ''
373 self.vstatus.cur_user = ''
374 self.vstatus.cur_selection = ''
375 self._last_card_id = -1
376 self.dispense.logOut()
377 self.vstatus.mk.set_messages([(self.center('BYE!'), False, 1.5)])
380 self.vstatus.cur_selection += chr(key + ord('0'))
381 self.vstatus.mk.set_message('SELECT: '+self.vstatus.cur_selection)
382 self.vstatus.time_to_autologout = None
383 elif len(self.vstatus.cur_selection) == 1:
385 self.vstatus.cur_selection = ''
386 self.vstatus.time_to_autologout = None
387 self.dispense.logOut()
388 self.scroll_options(self.vstatus.username, self.vstatus.mk)
391 self.vstatus.cur_selection += chr(key + ord('0'))
392 if self.dispense.isLoggedIn():
393 self.make_selection()
394 self.vstatus.cur_selection = ''
395 self.vstatus.time_to_autologout = time() + 8
396 self.vstatus.last_timeout_refresh = None
399 (name,price) = self.dispense.getItemInfo(self.vstatus.cur_selection)
400 dollarprice = "$%.2f" % ( price / 100.0 )
401 dollarprice = dollarprice[:-4] + '.' + dollarprice[-4] + dollarprice[-2:] # Work around display bug
402 self.v.display( self.vstatus.cur_selection+' - %s'%dollarprice)
404 self.vstatus.cur_selection = ''
405 self.vstatus.time_to_autologout = None
406 self.vstatus.last_timeout_refresh = None
409 Triggered when the user has entered the id of something they would like to purchase.
411 def make_selection(self):
412 logging.debug('Dispense item "%s"' % (self.vstatus.cur_selection,))
413 # should use sudo here
414 if self.vstatus.cur_selection == '55':
415 self.vstatus.mk.set_message('OPENSESAME')
416 logging.info('dispensing a door for %s'%self.vstatus.username)
418 ret = os.system('dispense -u "%s" door'%self.vstatus.username)
420 ret = os.system('dispense door')
422 logging.info('door opened')
423 self.vstatus.mk.set_message(self.center('DOOR OPEN'))
425 logging.warning('user %s tried to dispense a bad door'%self.vstatus.username)
426 self.vstatus.mk.set_message(self.center('BAD DOOR'))
428 elif self.vstatus.cur_selection == '81':
430 elif self.vstatus.cur_selection == '99':
431 self.scroll_options(self.vstatus.username, self.vstatus.mk)
432 self.vstatus.cur_selection = ''
434 elif self.vstatus.cur_selection[1] == '8':
435 self.v.display('GOT DRINK?')
436 if ((os.system('dispense -u "%s" coke:%s'%(self.vstatus.username, self.vstatus.cur_selection[0])) >> 8) != 0):
437 self.v.display('SEEMS NOT')
439 self.v.display('GOT DRINK!')
441 # first see if it's a named slot
443 price, shortname, name = get_snack( self.vstatus.cur_selection )
445 price, shortname, name = get_snack( '--' )
446 dollarprice = "$%.2f" % ( price / 100.0 )
447 self.v.display(self.vstatus.cur_selection+' - %s'%dollarprice)
448 exitcode = os.system('dispense -u "%s" snack:%s'%(self.vstatus.username, self.vstatus.cur_selection)) >> 8
450 # magic dispense syslog service
451 (worked, code, string) = self.v.vend(self.vstatus.cur_selection)
453 self.v.display('THANK YOU')
454 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, self.vstatus.cur_selection, self.vstatus.username))
456 print "Vend Failed:", code, string
457 syslog.syslog(syslog.LOG_WARNING | syslog.LOG_LOCAL4, "vending %s (slot %s) for %s FAILED %r %r" % (name, self.vstatus.cur_selection, self.vstatus.username, code, string))
458 self.v.display('VEND FAIL')
459 elif (exitcode == 5): # RV_BALANCE
460 self.v.display('NO MONEY?')
461 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
462 self.v.display('EMPTY SLOT')
463 elif (exitcode == 1): # RV_BADITEM (Dead slot)
464 self.v.display('EMPTY SLOT')
466 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))
467 self.v.display('UNK ERROR')
471 Triggered when the user presses a button while entering their pin.
473 def handle_getting_pin_key(self, event, params):
475 if len(self.vstatus.cur_pin) < PIN_LENGTH:
477 if self.vstatus.cur_pin == '':
478 self.vstatus.cur_user = ''
479 self.dispense.logOut()
483 self.vstatus.cur_pin = ''
484 self.vstatus.mk.set_message('PIN: ')
486 self.vstatus.cur_pin += chr(key + ord('0'))
487 self.vstatus.mk.set_message('PIN: '+'X'*len(self.vstatus.cur_pin))
488 if len(self.vstatus.cur_pin) == PIN_LENGTH:
489 if self.dispense.authUserIdPin(self.vstatus.cur_user, self.vstatus.cur_pin):
490 self.vstatus.username = self.dispense.getUsername()
491 self.v.beep(0, False)
492 self.vstatus.cur_selection = ''
493 self.vstatus.change_state(STATE_GET_SELECTION)
494 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
497 self.v.beep(40, False)
498 self.vstatus.mk.set_messages(
499 [(self.center('BAD PIN'), False, 1.0),
500 (self.center('SORRY'), False, 0.5)])
501 self.vstatus.cur_user = ''
502 self.vstatus.cur_pin = ''
509 Triggered when the user presses a button while entering their user id.
511 def handle_getting_uid_key(self, event, params):
513 # complicated key handling here:
515 if len(self.vstatus.cur_user) == 0 and key == 9:
516 self.vstatus.cur_selection = ''
517 self.vstatus.time_to_autologout = None
518 self.vstatus.mk.set_message('PRICECHECK')
520 self.scroll_options('', self.vstatus.mk)
521 self.vstatus.change_state(STATE_GET_SELECTION)
524 if len(self.vstatus.cur_user) <8:
526 self.vstatus.cur_user = ''
527 self.dispense.logOut()
531 self.vstatus.cur_user += chr(key + ord('0'))
532 #logging.info('dob: '+vstatus.cur_user)
533 if len(self.vstatus.cur_user) > 5:
534 self.vstatus.mk.set_message('>'+self.vstatus.cur_user)
536 self.vstatus.mk.set_message('UID: '+self.vstatus.cur_user)
538 if len(self.vstatus.cur_user) == 5:
539 uid = int(self.vstatus.cur_user)
542 logging.info('user '+self.vstatus.cur_user+' has a bad PIN')
548 Welcome to Picklevision Sytems, Sunnyvale, CA
550 Greetings Professor Falken.
555 Shall we play a game?
558 Please choose from the following menu:
565 6. Toxic and Biochemical Warfare
566 7. Global Thermonuclear War
570 Wouldn't you prefer a nice game of chess?
572 """.replace('\n',' ')
573 self.vstatus.mk.set_messages([(pfalken, False, 10)])
574 self.vstatus.cur_user = ''
575 self.vstatus.cur_pin = ''
581 # TODO Fix this up do we can check before logging in
583 if self.dispense.isDisabled():
584 logging.info('user '+self.vstatus.cur_user+' is disabled')
585 self.vstatus.mk.set_messages(
586 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
587 self.vstatus.cur_user = ''
588 self.vstatus.cur_pin = ''
594 self.vstatus.cur_pin = ''
595 self.vstatus.mk.set_message('PIN: ')
596 logging.info('need pin for user %s'%self.vstatus.cur_user)
597 self.vstatus.change_state(STATE_GETTING_PIN)
601 Triggered when a key is pressed and the machine is idling.
603 def handle_idle_key(self, event, params):
606 self.vstatus.cur_user = ''
607 self.dispense.logOut()
611 self.vstatus.change_state(STATE_GETTING_UID)
612 self.run_handler(event, params)
615 What to do when there is nothing to do.
617 def handle_idle_tick(self, event, params):
619 if self.vstatus.mk.done():
622 if self.vstatus.time_of_next_idler and time() > self.vstatus.time_of_next_idler:
623 self.vstatus.time_of_next_idler = time() + 30
628 self.vstatus.mk.update_display()
630 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK)
631 self.run_handler(event, params)
635 Manages the beeps for the grandfather clock
637 def beep_on(self, when, before=0):
638 start = int(when - before)
642 if now >= start and now <= end:
646 def handle_idle_grandfather_tick(self, event, params):
647 ### check for interesting times
650 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
651 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
652 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
653 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
655 hourfromnow = localtime(time() + 3600)
657 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
658 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
659 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
661 ## check for X seconds to the hour
662 ## if case, update counter to 2
663 if self.beep_on(onthehour,15) \
664 or self.beep_on(halfhour,0) \
665 or self.beep_on(quarterhour,0) \
666 or self.beep_on(threequarterhour,0) \
667 or self.beep_on(fivetothehour,0):
668 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
669 self.run_handler(event, params)
671 self.vstatus.change_state(STATE_IDLE)
673 def handle_grandfather_tick(self, event, params):
677 ### we live in interesting times
680 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
681 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
682 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
683 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
685 hourfromnow = localtime(time() + 3600)
687 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
688 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
689 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
692 #print "when it fashionable to wear a onion on your hip"
694 if self.beep_on(onthehour,15):
696 next_hour=((hourfromnow[3] + 11) % 12) + 1
697 if onthehour - time() < next_hour and onthehour - time() > 0:
698 self.v.beep(0, False)
702 msg.append(("DING!", False, None))
704 msg.append((" DING!", False, None))
705 elif int(onthehour - time()) == 0:
706 self.v.beep(255, False)
707 msg.append((" BONG!", False, None))
708 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
709 elif self.beep_on(halfhour,0):
711 self.v.beep(0, False)
712 msg.append((" HALFHOUR ", False, 50))
713 elif self.beep_on(quarterhour,0):
715 self.v.beep(0, False)
716 msg.append((" QTR HOUR ", False, 50))
717 elif self.beep_on(threequarterhour,0):
719 self.v.beep(0, False)
720 msg.append((" 3 QTR HR ", False, 50))
721 elif self.beep_on(fivetothehour,0):
723 self.v.beep(0, False)
724 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
728 ## check for X seconds to the hour
731 self.vstatus.mk.set_messages(msg)
734 self.vstatus.mk.update_display()
735 ## if no longer case, return to idle
737 ## change idler to be clock
738 if go_idle and self.vstatus.mk.done():
739 self.vstatus.change_state(STATE_IDLE,1)
742 What to do when the door is open.
744 def handle_door_idle(self, event, params):
745 def twiddle(clock,v,wise = 2):
747 v.display("-FEED ME-")
748 elif (clock % 4 == 1+wise):
749 v.display("\\FEED ME/")
750 elif (clock % 4 == 2):
751 v.display("-FEED ME-")
752 elif (clock % 4 == 3-wise):
753 v.display("/FEED ME\\")
755 # don't care right now.
758 if ((now % 60 % 2) == 0):
761 twiddle(now, self.v, wise=0)
764 What to do when the door is opened or closed.
766 def handle_door_event(self, event, params):
767 if params == 0: #door open
768 self.vstatus.change_state(STATE_DOOR_OPENING)
769 logging.warning("Entering open door mode")
770 self.v.display("-FEED ME-")
772 self.dispense.logOut()
773 self.vstatus.cur_user = ''
774 self.vstatus.cur_pin = ''
775 elif params == 1: #door closed
776 self.vstatus.change_state(STATE_DOOR_CLOSING)
779 logging.warning('Leaving open door mode')
780 self.v.display("-YUM YUM!-")
783 Triggered when a user swipes their caed, and the machine is logged out.
785 def handle_mifare_event(self, event, params):
787 # Translate card_id into uid.
788 if card_id == None or card_id == self._last_card_id:
791 self._last_card_id = card_id
793 if not self.dispense.authMifareCard(card_id):
794 self.v.beep(40, False)
795 self.vstatus.mk.set_messages(
796 [(self.center('BAD CARD'), False, 1.0),
797 (self.center('SORRY'), False, 0.5)])
798 self.vstatus.cur_user = ''
799 self.vstatus.cur_pin = ''
800 self._last_card_id = -1
804 elif self.dispense.isDisabled():
805 logging.info('Mapped card id to uid %s'%self.dispense.getUsername())
806 self.v.beep(40, False)
807 self.vstatus.mk.set_messages(
808 [(self.center('ACCT DISABLED'), False, 1.0),
809 (self.center('SORRY'), False, 0.5)])
810 self.dispense.logOut()
814 logging.info('Mapped card id to uid %s'%self.dispense.getUsername())
815 self.vstatus.cur_user = '----'
816 self.vstatus.username = self.dispense.getUsername()
817 self.vstatus.cur_selection = ''
818 self.vstatus.change_state(STATE_GET_SELECTION)
819 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
823 Triggered when a user swipes their card and the machine is logged in.
825 def handle_mifare_add_user_event(self, event, params):
828 # Translate card_id into uid.
829 if card_id == None or card_id == self._last_card_id:
832 self._last_card_id = card_id
834 if not self.dispense.addCard(card_id):
835 self.vstatus.mk.set_messages(
836 [(self.center('ALREADY'), False, 0.5),
837 (self.center('ENROLLED'), False, 0.5)])
839 self.vstatus.mk.set_messages(
840 [(self.center('CARD'), False, 0.5),
841 (self.center('ENROLLED'), False, 0.5)])
843 def return_to_idle(self, event, params):
847 Maps what to do when the state changes.
849 def create_state_table(self):
850 self.vstatus.state_table[(STATE_IDLE,TICK,1)] = self.handle_idle_tick
851 self.vstatus.state_table[(STATE_IDLE,KEY,1)] = self.handle_idle_key
852 self.vstatus.state_table[(STATE_IDLE,DOOR,1)] = self.handle_door_event
853 self.vstatus.state_table[(STATE_IDLE,MIFARE,1)] = self.handle_mifare_event
855 self.vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = self.handle_door_idle
856 self.vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = self.handle_door_event
857 self.vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = self.do_nothing
858 self.vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = self.do_nothing
860 self.vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = self.return_to_idle
861 self.vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = self.handle_door_event
862 self.vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = self.do_nothing
863 self.vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = self.do_nothing
865 self.vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = self.handle_getting_uid_idle
866 self.vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = self.handle_door_event
867 self.vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = self.handle_getting_uid_key
868 self.vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = self.handle_mifare_event
870 self.vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = self.handle_getting_pin_idle
871 self.vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = self.handle_door_event
872 self.vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = self.handle_getting_pin_key
873 self.vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = self.handle_mifare_event
875 self.vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = self.handle_get_selection_idle
876 self.vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = self.handle_door_event
877 self.vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = self.handle_get_selection_key
878 self.vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = self.handle_mifare_add_user_event
880 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = self.handle_idle_grandfather_tick
881 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = self.handle_grandfather_tick
882 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = self.handle_door_event
883 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = self.handle_door_event
884 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = self.do_nothing
885 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = self.do_nothing
886 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = self.handle_mifare_event
889 Get what to do on a state change.
891 def get_state_table_handler(self, state, event, counter):
892 return self.vstatus.state_table[(state,event,counter)]
894 def time_to_next_update(self):
895 idle_update = self.vstatus.time_of_next_idlestep - time()
896 if not self.vstatus.mk.done() and self.vstatus.mk.next_update is not None:
897 mk_update = self.vstatus.mk.next_update - time()
898 if mk_update < idle_update:
899 idle_update = mk_update
902 def run_forever(self, rfh, wfh, options, cf):
903 self.v = VendingMachine(rfh, wfh, USE_MIFARE)
904 self.dispense = Dispense()
905 self.vstatus = VendState(self.v)
906 self.create_state_table()
908 logging.debug('PING is ' + str(self.v.ping()))
914 timeout = self.time_to_next_update()
915 (event, params) = self.v.next_event(timeout)
916 self.run_handler(event, params)
918 def run_handler(self, event, params):
919 handler = self.get_state_table_handler(self.vstatus.state,event,self.vstatus.counter)
921 handler(event, params)
924 Connect to the machine.
926 def connect_to_vend(options, cf):
929 logging.info('Connecting to vending machine using LAT')
930 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
931 rfh, wfh = latclient.get_fh()
932 elif options.use_serial:
933 # Open vending machine via serial.
934 logging.info('Connecting to vending machine using serial')
935 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
936 rfh,wfh = serialclient.get_fh()
938 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
939 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
941 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
942 sock.connect((options.host, options.port))
943 rfh = sock.makefile('r')
944 wfh = sock.makefile('w')
951 Parse arguments from the command line
954 from optparse import OptionParser
956 op = OptionParser(usage="%prog [OPTION]...")
957 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')
958 op.add_option('--serial', action='store_true', default=False, dest='use_serial', help='use the serial port')
959 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
960 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
961 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
962 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
963 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
964 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
965 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
966 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
967 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
968 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
969 options, args = op.parse_args()
972 op.error('extra command line arguments: ' + ' '.join(args))
976 def create_pid_file(name):
978 pid_file = file(name, 'w')
979 pid_file.write('%d\n'%os.getpid())
982 logging.warning('unable to write to pid file '+name+': '+str(e))
985 def do_nothing(signum, stack):
986 signal.signal(signum, do_nothing)
987 def stop_server(signum, stack): raise KeyboardInterrupt
988 signal.signal(signal.SIGHUP, do_nothing)
989 signal.signal(signal.SIGTERM, stop_server)
990 signal.signal(signal.SIGINT, stop_server)
992 options = parse_args()
993 config_opts = VendConfigFile(options.config_file, config_options)
994 if options.daemon: become_daemon()
995 set_up_logging(options)
996 if options.pid_file != '': create_pid_file(options.pid_file)
998 return options, config_opts
1000 def clean_up_nicely(options, config_opts):
1001 if options.pid_file != '':
1003 os.unlink(options.pid_file)
1004 logging.debug('Removed pid file '+options.pid_file)
1005 except OSError: pass # if we can't delete it, meh
1007 def set_up_logging(options):
1008 logger = logging.getLogger()
1010 if not options.daemon:
1011 stderr_logger = logging.StreamHandler(sys.stderr)
1012 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1013 logger.addHandler(stderr_logger)
1015 if options.log_file != '':
1017 file_logger = logging.FileHandler(options.log_file)
1018 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1019 logger.addHandler(file_logger)
1021 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1023 if options.syslog != None:
1024 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1025 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1026 logger.addHandler(sys_logger)
1029 logger.setLevel(logging.WARNING)
1030 elif options.verbose:
1031 logger.setLevel(logging.DEBUG)
1033 logger.setLevel(logging.INFO)
1035 def become_daemon():
1036 dev_null = file('/dev/null')
1037 fd = dev_null.fileno()
1046 raise SystemExit('failed to fork: '+str(e))
1048 def do_vend_server(options, config_opts):
1051 rfh, wfh = connect_to_vend(options, config_opts)
1052 except (SerialClientException, socket.error), e:
1053 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1055 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1056 logging.info("Trying again in 5 seconds.")
1060 # run_forever(rfh, wfh, options, config_opts)
1063 vserver = VendServer()
1064 vserver.run_forever(rfh, wfh, options, config_opts)
1065 except VendingException:
1066 logging.error("Connection died, trying again...")
1067 logging.info("Trying again in 5 seconds.")
1071 def main(argv=None):
1072 options, config_opts = set_stuff_up()
1075 logging.warning('Starting Vend Server')
1076 do_vend_server(options, config_opts)
1077 logging.error('Vend Server finished unexpectedly, restarting')
1078 except KeyboardInterrupt:
1079 logging.info("Killed by signal, cleaning up")
1080 clean_up_nicely(options, config_opts)
1081 logging.warning("Vend Server stopped")
1086 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1087 tb = format_tb(exc_traceback, 20)
1090 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1091 logging.critical("Message: " + str(exc_value))
1092 logging.critical("Traceback:")
1094 for line in event.split('\n'):
1095 logging.critical(' '+line)
1096 logging.critical("This message should be considered a bug in the Vend Server.")
1097 logging.critical("Please report this to someone who can fix it.")
1099 logging.warning("Trying again anyway (might not help, but hey...)")
1101 if __name__ == '__main__':