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
23 import TracebackPrinter
26 This vending machine software brought to you by:
31 and a collective of hungry alpacas.
33 The MIFARE card reader bought to you by:
36 Bug Hunting and hardware maintenance by:
39 For a good time call +61 8 6488 3901
61 STATE_GRANDFATHER_CLOCK,
69 'DBServer': ('Database', 'Server'),
70 'DBName': ('Database', 'Name'),
71 'DBUser': ('VendingMachine', 'DBUser'),
72 'DBPassword': ('VendingMachine', 'DBPassword'),
74 'ServiceName': ('VendingMachine', 'ServiceName'),
75 'ServicePassword': ('VendingMachine', 'Password'),
77 'ServerName': ('DecServer', 'Name'),
78 'ConnectPassword': ('DecServer', 'ConnectPassword'),
79 'PrivPassword': ('DecServer', 'PrivPassword'),
83 def __init__(self, config_file, options):
85 cp = ConfigParser.ConfigParser()
88 for option in options:
89 section, name = options[option]
90 value = cp.get(section, name)
91 self.__dict__[option] = value
93 except ConfigParser.Error, e:
94 raise SystemExit("Error reading config file "+config_file+": " + str(e))
97 This class manages the current state of the vending machine.
100 def __init__(self,v):
101 self.state_table = {}
102 self.state = STATE_IDLE
105 self.mk = MessageKeeper(v)
109 self.cur_selection = ''
110 self.time_to_autologout = None
112 self.last_timeout_refresh = None
114 def change_state(self,newstate,newcounter=None):
115 if self.state != newstate:
116 self.state = newstate
118 if newcounter is not None and self.counter != newcounter:
119 self.counter = newcounter
138 Show information to the user as to what can be dispensed.
140 def scroll_options(self, username, mk, welcome = False):
141 # If the user has just logged in, show them their balance
143 balance = self.dispense.getBalance()
144 msg = [(self.center('WELCOME'), False, TEXT_SPEED),
145 (self.center(self.dispense.getUsername()), False, TEXT_SPEED),
146 (self.center(balance), False, TEXT_SPEED),]
149 choices = ' '*10+'CHOICES: '
151 # Show what is in the coke machine
152 # Need to update this so it uses the abstracted system
154 for i in ['08', '18', '28', '38', '48', '58', '68']:
155 cokes.append((i, self.dispense.getItemInfo(i)))
158 if c[1][0] == 'dead':
160 choices += '%s-(%sc)-%s8 '%(c[1][0], c[1][1], c[0])
162 # Show the final few options
163 choices += '55-DOOR '
164 choices += 'OR ANOTHER SNACK. '
165 choices += '99 TO READ AGAIN. '
166 choices += 'CHOICE? '
167 msg.append((choices, False, None))
168 # Send it to the display
172 In here just for fun.
176 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
177 choice = int(random()*len(messages))
178 msg = messages[choice]
179 left = range(len(msg))
180 for i in range(len(msg)):
181 if msg[i] == ' ': left.remove(i)
185 for i in range(0, len(msg)):
191 s += chr(int(random()*26)+ord('A'))
199 Format text so it will appear centered on the screen.
201 def center(self, str):
203 return ' '*((LEN-len(str))/2)+str
206 Configure the things that will appear on screen whil the machine is idling.
208 def setup_idlers(self):
212 GrayIdler(self.v,one="*",zero="-"),
213 GrayIdler(self.v,one="/",zero="\\"),
214 GrayIdler(self.v,one="X",zero="O"),
215 GrayIdler(self.v,one="*",zero="-",reorder=1),
216 GrayIdler(self.v,one="/",zero="\\",reorder=1),
217 GrayIdler(self.v,one="X",zero="O",reorder=1),
223 StringIdler(self.v), # Hello Cruel World
224 StringIdler(self.v, text="Kill 'em all", repeat=False),
225 StringIdler(self.v, text=CREDITS),
226 StringIdler(self.v, text=str(math.pi) + " "),
227 StringIdler(self.v, text=str(math.e) + " "),
228 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),
229 # "Hello World" in brainfuck
230 StringIdler(self.v, text=">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++."),
234 FileIdler(self.v, '/usr/share/common-licenses/GPL-2',affinity=2),
236 PipeIdler(self.v, "/usr/bin/getent", "passwd"),
237 FortuneIdler(self.v,affinity=20),
243 Go back to the default idler.
245 def reset_idler(self, t = None):
246 self.idler = GreetingIdler(self.v, t)
247 self.vstatus.time_of_next_idlestep = time()+self.idler.next()
248 self.vstatus.time_of_next_idler = None
249 self.vstatus.time_to_autologout = None
250 self.vstatus.change_state(STATE_IDLE, 1)
253 Change to a random idler.
255 def choose_idler(self):
257 # Implementation of the King Of the Hill algorithm from;
258 # http://eli.thegreenplace.net/2010/01/22/weighted-random-generation-in-python/
260 #def weighted_choice_king(weights):
263 # for i, w in enumerate(weights):
265 # if random.random() * total < w:
273 for choice in self.idlers:
274 weight = choice.affinity()
276 if random() * total < weight:
284 Run every step while the machine is idling.
287 if self.idler.finished():
289 self.vstatus.time_of_next_idler = time() + 30
290 nextidle = self.idler.next()
292 nextidle = IDLE_SPEED
293 self.vstatus.time_of_next_idlestep = time()+nextidle
296 These next two events trigger no response in the code.
298 def handle_tick_event(self, event, params):
299 # don't care right now.
302 def handle_switch_event(self, event, params):
303 # don't care right now.
307 Don't do anything for this event.
309 def do_nothing(self, event, params):
310 print "doing nothing (s,e,p)", state, " ", event, " ", params
314 These next few entrie tell us to do nothing while we are idling
316 def handle_getting_uid_idle(self, event, params):
317 # don't care right now.
320 def handle_getting_pin_idle(self, event, params):
321 # don't care right now.
325 While logged in and waiting for user input, slowly get closer to logging out.
327 def handle_get_selection_idle(self, event, params):
328 # don't care right now.
330 ### State logging out ..
331 if self.vstatus.time_to_autologout != None:
332 time_left = self.vstatus.time_to_autologout - time()
333 if time_left < 6 and (self.vstatus.last_timeout_refresh is None or self.vstatus.last_timeout_refresh > time_left):
334 self.vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
335 self.vstatus.last_timeout_refresh = int(time_left)
336 self.vstatus.cur_selection = ''
338 # Login timed out: Log out the current user.
339 if self.vstatus.time_to_autologout != None and self.vstatus.time_to_autologout - time() <= 0:
340 self.vstatus.time_to_autologout = None
341 self.vstatus.cur_user = ''
342 self.vstatus.cur_pin = ''
343 self.vstatus.cur_selection = ''
344 self._last_card_id = -1
345 self.dispense.logOut()
348 ### State fully logged out ... reset variables
349 if self.vstatus.time_to_autologout and not self.vstatus.mk.done():
350 self.vstatus.time_to_autologout = None
351 if self.vstatus.cur_user == '' and self.vstatus.time_to_autologout:
352 self.vstatus.time_to_autologout = None
355 if len(self.vstatus.cur_pin) == PIN_LENGTH and self.vstatus.mk.done() and self.vstatus.time_to_autologout == None:
357 self.vstatus.time_to_autologout = time() + 15
358 self.vstatus.last_timeout_refresh = None
360 ## FIXME - this may need to be elsewhere.....
362 self.vstatus.mk.update_display()
365 Triggered on user input while logged in.
367 def handle_get_selection_key(self, event, params):
369 if len(self.vstatus.cur_selection) == 0:
371 self.vstatus.cur_pin = ''
372 self.vstatus.cur_user = ''
373 self.vstatus.cur_selection = ''
374 self._last_card_id = -1
375 self.dispense.logOut()
376 self.vstatus.mk.set_messages([(self.center('BYE!'), False, 1.5)])
379 self.vstatus.cur_selection += chr(key + ord('0'))
380 self.vstatus.mk.set_message('SELECT: '+self.vstatus.cur_selection)
381 self.vstatus.time_to_autologout = None
382 elif len(self.vstatus.cur_selection) == 1:
384 self.vstatus.cur_selection = ''
385 self.vstatus.time_to_autologout = None
386 self.dispense.logOut()
387 self.scroll_options(self.vstatus.username, self.vstatus.mk)
390 self.vstatus.cur_selection += chr(key + ord('0'))
391 if self.dispense.isLoggedIn():
392 self.make_selection()
393 self.vstatus.cur_selection = ''
394 self.vstatus.time_to_autologout = time() + 8
395 self.vstatus.last_timeout_refresh = None
398 (name,price) = self.dispense.getItemInfo(self.vstatus.cur_selection)
399 dollarprice = "$%.2f" % ( price / 100.0 )
400 self.v.display( self.vstatus.cur_selection+' - %s'%dollarprice)
402 self.vstatus.cur_selection = ''
403 self.vstatus.time_to_autologout = None
404 self.vstatus.last_timeout_refresh = None
407 Triggered when the user has entered the id of something they would like to purchase.
409 def make_selection(self):
410 logging.debug('Dispense item "%s"' % (self.vstatus.cur_selection,))
411 # should use sudo here
412 if self.vstatus.cur_selection == '55':
413 self.vstatus.mk.set_message('OPENSESAME')
414 logging.info('dispensing a door for %s'%self.vstatus.username)
416 ret = os.system('dispense -u "%s" door'%self.vstatus.username)
418 ret = os.system('dispense door')
420 logging.info('door opened')
421 self.vstatus.mk.set_message(self.center('DOOR OPEN'))
423 logging.warning('user %s tried to dispense a bad door'%self.vstatus.username)
424 self.vstatus.mk.set_message(self.center('BAD DOOR'))
426 elif self.vstatus.cur_selection == '81':
428 elif self.vstatus.cur_selection == '99':
429 self.scroll_options(self.vstatus.username, self.vstatus.mk)
430 self.vstatus.cur_selection = ''
432 elif self.vstatus.cur_selection[1] == '8':
433 self.v.display('GOT DRINK?')
434 if ((os.system('dispense -u "%s" coke:%s'%(self.vstatus.username, self.vstatus.cur_selection[0])) >> 8) != 0):
435 self.v.display('SEEMS NOT')
437 self.v.display('GOT DRINK!')
439 # first see if it's a named slot
441 price, shortname, name = get_snack( self.vstatus.cur_selection )
443 price, shortname, name = get_snack( '--' )
444 dollarprice = "$%.2f" % ( price / 100.0 )
445 self.v.display(self.vstatus.cur_selection+' - %s'%dollarprice)
446 exitcode = os.system('dispense -u "%s" snack:%s'%(self.vstatus.username, self.vstatus.cur_selection)) >> 8
448 # magic dispense syslog service
449 (worked, code, string) = self.v.vend(self.vstatus.cur_selection)
451 self.v.display('THANK YOU')
452 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, self.vstatus.cur_selection, self.vstatus.username))
454 print "Vend Failed:", code, string
455 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))
456 self.v.display('VEND FAIL')
457 elif (exitcode == 5): # RV_BALANCE
458 self.v.display('NO MONEY?')
459 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
460 self.v.display('EMPTY SLOT')
461 elif (exitcode == 1): # RV_BADITEM (Dead slot)
462 self.v.display('EMPTY SLOT')
464 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))
465 self.v.display('UNK ERROR')
469 Triggered when the user presses a button while entering their pin.
471 def handle_getting_pin_key(self, event, params):
473 if len(self.vstatus.cur_pin) < PIN_LENGTH:
475 if self.vstatus.cur_pin == '':
476 self.vstatus.cur_user = ''
477 self.dispense.logOut()
481 self.vstatus.cur_pin = ''
482 self.vstatus.mk.set_message('PIN: ')
484 self.vstatus.cur_pin += chr(key + ord('0'))
485 self.vstatus.mk.set_message('PIN: '+'X'*len(self.vstatus.cur_pin))
486 if len(self.vstatus.cur_pin) == PIN_LENGTH:
487 if self.dispense.authUserIdPin(self.vstatus.cur_user, self.vstatus.cur_pin):
488 self.vstatus.username = self.dispense.getUsername()
489 self.v.beep(0, False)
490 self.vstatus.cur_selection = ''
491 self.vstatus.change_state(STATE_GET_SELECTION)
492 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
495 self.v.beep(40, False)
496 self.vstatus.mk.set_messages(
497 [(self.center('BAD PIN'), False, 1.0),
498 (self.center('SORRY'), False, 0.5)])
499 self.vstatus.cur_user = ''
500 self.vstatus.cur_pin = ''
507 Triggered when the user presses a button while entering their user id.
509 def handle_getting_uid_key(self, event, params):
511 # complicated key handling here:
513 if len(self.vstatus.cur_user) == 0 and key == 9:
514 self.vstatus.cur_selection = ''
515 self.vstatus.time_to_autologout = None
516 self.vstatus.mk.set_message('PRICECHECK')
518 self.scroll_options('', self.vstatus.mk)
519 self.vstatus.change_state(STATE_GET_SELECTION)
522 if len(self.vstatus.cur_user) <8:
524 self.vstatus.cur_user = ''
525 self.dispense.logOut()
529 self.vstatus.cur_user += chr(key + ord('0'))
530 #logging.info('dob: '+vstatus.cur_user)
531 if len(self.vstatus.cur_user) > 5:
532 self.vstatus.mk.set_message('>'+self.vstatus.cur_user)
534 self.vstatus.mk.set_message('UID: '+self.vstatus.cur_user)
536 if len(self.vstatus.cur_user) == 5:
537 uid = int(self.vstatus.cur_user)
540 logging.info('user '+self.vstatus.cur_user+' has a bad PIN')
546 Welcome to Picklevision Sytems, Sunnyvale, CA
548 Greetings Professor Falken.
553 Shall we play a game?
556 Please choose from the following menu:
563 6. Toxic and Biochemical Warfare
564 7. Global Thermonuclear War
568 Wouldn't you prefer a nice game of chess?
570 """.replace('\n',' ')
571 self.vstatus.mk.set_messages([(pfalken, False, 10)])
572 self.vstatus.cur_user = ''
573 self.vstatus.cur_pin = ''
579 # TODO Fix this up do we can check before logging in
581 if self.dispense.isDisabled():
582 logging.info('user '+self.vstatus.cur_user+' is disabled')
583 self.vstatus.mk.set_messages(
584 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
585 self.vstatus.cur_user = ''
586 self.vstatus.cur_pin = ''
592 self.vstatus.cur_pin = ''
593 self.vstatus.mk.set_message('PIN: ')
594 logging.info('need pin for user %s'%self.vstatus.cur_user)
595 self.vstatus.change_state(STATE_GETTING_PIN)
599 Triggered when a key is pressed and the machine is idling.
601 def handle_idle_key(self, event, params):
604 self.vstatus.cur_user = ''
605 self.dispense.logOut()
609 self.vstatus.change_state(STATE_GETTING_UID)
610 self.run_handler(event, params)
613 What to do when there is nothing to do.
615 def handle_idle_tick(self, event, params):
617 if self.vstatus.mk.done():
620 if self.vstatus.time_of_next_idler and time() > self.vstatus.time_of_next_idler:
621 self.vstatus.time_of_next_idler = time() + 30
626 self.vstatus.mk.update_display()
628 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK)
629 self.run_handler(event, params)
633 Manages the beeps for the grandfather clock
635 def beep_on(self, when, before=0):
636 start = int(when - before)
640 if now >= start and now <= end:
644 def handle_idle_grandfather_tick(self, event, params):
645 ### check for interesting times
648 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
649 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
650 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
651 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
653 hourfromnow = localtime(time() + 3600)
655 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
656 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
657 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
659 ## check for X seconds to the hour
660 ## if case, update counter to 2
661 if self.beep_on(onthehour,15) \
662 or self.beep_on(halfhour,0) \
663 or self.beep_on(quarterhour,0) \
664 or self.beep_on(threequarterhour,0) \
665 or self.beep_on(fivetothehour,0):
666 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
667 self.run_handler(event, params)
669 self.vstatus.change_state(STATE_IDLE)
671 def handle_grandfather_tick(self, event, params):
675 ### we live in interesting times
678 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
679 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
680 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
681 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
683 hourfromnow = localtime(time() + 3600)
685 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
686 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
687 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
690 #print "when it fashionable to wear a onion on your hip"
692 if self.beep_on(onthehour,15):
694 next_hour=((hourfromnow[3] + 11) % 12) + 1
695 if onthehour - time() < next_hour and onthehour - time() > 0:
696 self.v.beep(0, False)
700 msg.append(("DING!", False, None))
702 msg.append((" DING!", False, None))
703 elif int(onthehour - time()) == 0:
704 self.v.beep(255, False)
705 msg.append((" BONG!", False, None))
706 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
707 elif self.beep_on(halfhour,0):
709 self.v.beep(0, False)
710 msg.append((" HALFHOUR ", False, 50))
711 elif self.beep_on(quarterhour,0):
713 self.v.beep(0, False)
714 msg.append((" QTR HOUR ", False, 50))
715 elif self.beep_on(threequarterhour,0):
717 self.v.beep(0, False)
718 msg.append((" 3 QTR HR ", False, 50))
719 elif self.beep_on(fivetothehour,0):
721 self.v.beep(0, False)
722 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
726 ## check for X seconds to the hour
729 self.vstatus.mk.set_messages(msg)
732 self.vstatus.mk.update_display()
733 ## if no longer case, return to idle
735 ## change idler to be clock
736 if go_idle and self.vstatus.mk.done():
737 self.vstatus.change_state(STATE_IDLE,1)
740 What to do when the door is open.
742 def handle_door_idle(self, event, params):
743 def twiddle(clock,v,wise = 2):
745 v.display("-FEED ME-")
746 elif (clock % 4 == 1+wise):
747 v.display("\\FEED ME/")
748 elif (clock % 4 == 2):
749 v.display("-FEED ME-")
750 elif (clock % 4 == 3-wise):
751 v.display("/FEED ME\\")
753 # don't care right now.
756 if ((now % 60 % 2) == 0):
759 twiddle(now, self.v, wise=0)
762 What to do when the door is opened or closed.
764 def handle_door_event(self, event, params):
765 if params == 0: #door open
766 self.vstatus.change_state(STATE_DOOR_OPENING)
767 logging.warning("Entering open door mode")
768 self.v.display("-FEED ME-")
770 self.dispense.logOut()
771 self.vstatus.cur_user = ''
772 self.vstatus.cur_pin = ''
773 elif params == 1: #door closed
774 self.vstatus.change_state(STATE_DOOR_CLOSING)
777 logging.warning('Leaving open door mode')
778 self.v.display("-YUM YUM!-")
781 Triggered when a user swipes their caed, and the machine is logged out.
783 def handle_mifare_event(self, event, params):
785 # Translate card_id into uid.
786 if card_id == None or card_id == self._last_card_id:
789 self._last_card_id = card_id
791 if not self.dispense.authMifareCard(card_id):
792 self.v.beep(40, False)
793 self.vstatus.mk.set_messages(
794 [(self.center('BAD CARD'), False, 1.0),
795 (self.center('SORRY'), False, 0.5)])
796 self.vstatus.cur_user = ''
797 self.vstatus.cur_pin = ''
798 self._last_card_id = -1
802 elif self.dispense.isDisabled():
803 logging.info('Mapped card id to uid %s'%self.dispense.getUsername())
804 self.v.beep(40, False)
805 self.vstatus.mk.set_messages(
806 [(self.center('ACCT DISABLED'), False, 1.0),
807 (self.center('SORRY'), False, 0.5)])
808 self.dispense.logOut()
812 logging.info('Mapped card id to uid %s'%self.dispense.getUsername())
813 self.vstatus.cur_user = '----'
814 self.vstatus.username = self.dispense.getUsername()
815 self.vstatus.cur_selection = ''
816 self.vstatus.change_state(STATE_GET_SELECTION)
817 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
821 Triggered when a user swipes their card and the machine is logged in.
823 def handle_mifare_add_user_event(self, event, params):
826 # Translate card_id into uid.
827 if card_id == None or card_id == self._last_card_id:
830 self._last_card_id = card_id
832 if not self.dispense.addCard(card_id):
833 self.vstatus.mk.set_messages(
834 [(self.center('ALREADY'), False, 0.5),
835 (self.center('ENROLLED'), False, 0.5)])
837 self.vstatus.mk.set_messages(
838 [(self.center('CARD'), False, 0.5),
839 (self.center('ENROLLED'), False, 0.5)])
841 def return_to_idle(self, event, params):
845 Maps what to do when the state changes.
847 def create_state_table(self):
848 self.vstatus.state_table[(STATE_IDLE,TICK,1)] = self.handle_idle_tick
849 self.vstatus.state_table[(STATE_IDLE,KEY,1)] = self.handle_idle_key
850 self.vstatus.state_table[(STATE_IDLE,DOOR,1)] = self.handle_door_event
851 self.vstatus.state_table[(STATE_IDLE,MIFARE,1)] = self.handle_mifare_event
853 self.vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = self.handle_door_idle
854 self.vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = self.handle_door_event
855 self.vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = self.do_nothing
856 self.vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = self.do_nothing
858 self.vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = self.return_to_idle
859 self.vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = self.handle_door_event
860 self.vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = self.do_nothing
861 self.vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = self.do_nothing
863 self.vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = self.handle_getting_uid_idle
864 self.vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = self.handle_door_event
865 self.vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = self.handle_getting_uid_key
866 self.vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = self.handle_mifare_event
868 self.vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = self.handle_getting_pin_idle
869 self.vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = self.handle_door_event
870 self.vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = self.handle_getting_pin_key
871 self.vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = self.handle_mifare_event
873 self.vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = self.handle_get_selection_idle
874 self.vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = self.handle_door_event
875 self.vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = self.handle_get_selection_key
876 self.vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = self.handle_mifare_add_user_event
878 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = self.handle_idle_grandfather_tick
879 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = self.handle_grandfather_tick
880 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = self.handle_door_event
881 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = self.handle_door_event
882 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = self.do_nothing
883 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = self.do_nothing
884 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = self.handle_mifare_event
887 Get what to do on a state change.
889 def get_state_table_handler(self, state, event, counter):
890 return self.vstatus.state_table[(state,event,counter)]
892 def time_to_next_update(self):
893 idle_update = self.vstatus.time_of_next_idlestep - time()
894 if not self.vstatus.mk.done() and self.vstatus.mk.next_update is not None:
895 mk_update = self.vstatus.mk.next_update - time()
896 if mk_update < idle_update:
897 idle_update = mk_update
900 def run_forever(self, rfh, wfh, options, cf):
901 self.v = VendingMachine(rfh, wfh, USE_MIFARE)
902 self.dispense = Dispense()
903 self.vstatus = VendState(self.v)
904 self.create_state_table()
906 logging.debug('PING is ' + str(self.v.ping()))
912 timeout = self.time_to_next_update()
913 (event, params) = self.v.next_event(timeout)
914 self.run_handler(event, params)
916 def run_handler(self, event, params):
917 handler = self.get_state_table_handler(self.vstatus.state,event,self.vstatus.counter)
919 handler(event, params)
922 Connect to the machine.
924 def connect_to_vend(options, cf):
927 logging.info('Connecting to vending machine using LAT')
928 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
929 rfh, wfh = latclient.get_fh()
930 elif options.use_serial:
931 # Open vending machine via serial.
932 logging.info('Connecting to vending machine using serial')
933 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
934 rfh,wfh = serialclient.get_fh()
936 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
937 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
939 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
940 sock.connect((options.host, options.port))
941 rfh = sock.makefile('r')
942 wfh = sock.makefile('w')
949 Parse arguments from the command line
952 from optparse import OptionParser
954 op = OptionParser(usage="%prog [OPTION]...")
955 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')
956 op.add_option('--serial', action='store_true', default=False, dest='use_serial', help='use the serial port')
957 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
958 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
959 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
960 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
961 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
962 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
963 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
964 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
965 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
966 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
967 op.add_option('--traceback-file', dest='traceback_file', default='', help='destination to print tracebacks when receiving SIGUSR1')
968 options, args = op.parse_args()
971 op.error('extra command line arguments: ' + ' '.join(args))
975 def create_pid_file(name):
977 pid_file = file(name, 'w')
978 pid_file.write('%d\n'%os.getpid())
981 logging.warning('unable to write to pid file '+name+': '+str(e))
984 def do_nothing(signum, stack):
985 signal.signal(signum, do_nothing)
986 def stop_server(signum, stack): raise KeyboardInterrupt
987 signal.signal(signal.SIGHUP, do_nothing)
988 signal.signal(signal.SIGTERM, stop_server)
989 signal.signal(signal.SIGINT, stop_server)
991 options = parse_args()
992 config_opts = VendConfigFile(options.config_file, config_options)
993 if options.daemon: become_daemon()
994 set_up_logging(options)
995 if options.pid_file != '': create_pid_file(options.pid_file)
996 if options.traceback_file != '': TracebackPrinter.traceback_init(options.traceback_file)
997 return options, config_opts
999 def clean_up_nicely(options, config_opts):
1000 if options.pid_file != '':
1002 os.unlink(options.pid_file)
1003 logging.debug('Removed pid file '+options.pid_file)
1004 except OSError: pass # if we can't delete it, meh
1006 def set_up_logging(options):
1007 logger = logging.getLogger()
1009 if not options.daemon:
1010 stderr_logger = logging.StreamHandler(sys.stderr)
1011 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1012 logger.addHandler(stderr_logger)
1014 if options.log_file != '':
1016 file_logger = logging.FileHandler(options.log_file)
1017 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1018 logger.addHandler(file_logger)
1020 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1022 if options.syslog != None:
1023 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1024 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1025 logger.addHandler(sys_logger)
1028 logger.setLevel(logging.WARNING)
1029 elif options.verbose:
1030 logger.setLevel(logging.DEBUG)
1032 logger.setLevel(logging.INFO)
1034 def become_daemon():
1035 dev_null = file('/dev/null')
1036 fd = dev_null.fileno()
1045 raise SystemExit('failed to fork: '+str(e))
1047 def do_vend_server(options, config_opts):
1050 rfh, wfh = connect_to_vend(options, config_opts)
1051 except (SerialClientException, socket.error), e:
1052 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1054 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1055 logging.info("Trying again in 5 seconds.")
1059 # run_forever(rfh, wfh, options, config_opts)
1062 vserver = VendServer()
1063 vserver.run_forever(rfh, wfh, options, config_opts)
1064 except VendingException:
1065 logging.error("Connection died, trying again...")
1066 logging.info("Trying again in 5 seconds.")
1070 def main(argv=None):
1071 options, config_opts = set_stuff_up()
1074 logging.warning('Starting Vend Server')
1075 do_vend_server(options, config_opts)
1076 logging.error('Vend Server finished unexpectedly, restarting')
1077 except KeyboardInterrupt:
1078 logging.info("Killed by signal, cleaning up")
1079 clean_up_nicely(options, config_opts)
1080 logging.warning("Vend Server stopped")
1085 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1086 tb = format_tb(exc_traceback, 20)
1089 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1090 logging.critical("Message: " + str(exc_value))
1091 logging.critical("Traceback:")
1093 for line in event.split('\n'):
1094 logging.critical(' '+line)
1095 logging.critical("This message should be considered a bug in the Vend Server.")
1096 logging.critical("Please report this to someone who can fix it.")
1098 logging.warning("Trying again anyway (might not help, but hey...)")
1100 if __name__ == '__main__':