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 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
347 ### State fully logged out ... reset variables
348 if self.vstatus.time_to_autologout and not self.vstatus.mk.done():
349 self.vstatus.time_to_autologout = None
350 if self.vstatus.cur_user == '' and self.vstatus.time_to_autologout:
351 self.vstatus.time_to_autologout = None
354 if len(self.vstatus.cur_pin) == PIN_LENGTH and self.vstatus.mk.done() and self.vstatus.time_to_autologout == None:
356 self.vstatus.time_to_autologout = time() + 15
357 self.vstatus.last_timeout_refresh = None
359 ## FIXME - this may need to be elsewhere.....
361 self.vstatus.mk.update_display()
364 Triggered on user input while logged in.
366 def handle_get_selection_key(self, event, params):
368 if len(self.vstatus.cur_selection) == 0:
370 self.vstatus.cur_pin = ''
371 self.vstatus.cur_user = ''
372 self.vstatus.cur_selection = ''
373 self._last_card_id = -1
374 self.dispense.logOut()
375 self.vstatus.mk.set_messages([(self.center('BYE!'), False, 1.5)])
378 self.vstatus.cur_selection += chr(key + ord('0'))
379 self.vstatus.mk.set_message('SELECT: '+self.vstatus.cur_selection)
380 self.vstatus.time_to_autologout = None
381 elif len(self.vstatus.cur_selection) == 1:
383 self.vstatus.cur_selection = ''
384 self.vstatus.time_to_autologout = None
385 self.scroll_options(self.vstatus.username, self.vstatus.mk)
388 self.vstatus.cur_selection += chr(key + ord('0'))
389 if self.dispense.isLoggedIn():
390 self.make_selection()
391 self.vstatus.cur_selection = ''
392 self.vstatus.time_to_autologout = time() + 8
393 self.vstatus.last_timeout_refresh = None
396 (name,price) = self.dispense.getItemInfo(self.vstatus.cur_selection)
397 dollarprice = "$%.2f" % ( price / 100.0 )
398 dollarprice = dollarprice[:-4] + '.' + dollarprice[-4] + dollarprice[-2:] # Work around display bug
399 self.v.display( self.vstatus.cur_selection+' - %s'%dollarprice)
401 self.vstatus.cur_selection = ''
402 self.vstatus.time_to_autologout = None
403 self.vstatus.last_timeout_refresh = None
406 Triggered when the user has entered the id of something they would like to purchase.
408 def make_selection(self):
409 logging.debug('Dispense item "%s"' % (self.vstatus.cur_selection,))
410 # should use sudo here
411 if self.vstatus.cur_selection == '55':
412 self.vstatus.mk.set_message('OPENSESAME')
413 logging.info('dispensing a door for %s'%self.vstatus.username)
415 ret = os.system('dispense -u "%s" door'%self.vstatus.username)
417 ret = os.system('dispense door')
419 logging.info('door opened')
420 self.vstatus.mk.set_message(self.center('DOOR OPEN'))
422 logging.warning('user %s tried to dispense a bad door'%self.vstatus.username)
423 self.vstatus.mk.set_message(self.center('BAD DOOR'))
425 elif self.vstatus.cur_selection == '81':
427 elif self.vstatus.cur_selection == '99':
428 self.scroll_options(self.vstatus.username, self.vstatus.mk)
429 self.vstatus.cur_selection = ''
431 elif self.vstatus.cur_selection[1] == '8':
432 self.v.display('GOT DRINK?')
433 if ((os.system('dispense -u "%s" coke:%s'%(self.vstatus.username, self.vstatus.cur_selection[0])) >> 8) != 0):
434 self.v.display('SEEMS NOT')
436 self.v.display('GOT DRINK!')
438 # first see if it's a named slot
440 price, shortname, name = get_snack( self.vstatus.cur_selection )
442 price, shortname, name = get_snack( '--' )
443 dollarprice = "$%.2f" % ( price / 100.0 )
444 self.v.display(self.vstatus.cur_selection+' - %s'%dollarprice)
445 exitcode = os.system('dispense -u "%s" snack:%s'%(self.vstatus.username, self.vstatus.cur_selection)) >> 8
447 # magic dispense syslog service
448 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, self.vstatus.cur_selection, self.vstatus.username))
449 (worked, code, string) = self.v.vend(self.vstatus.cur_selection)
451 self.v.display('THANK YOU')
453 print "Vend Failed:", code, string
454 self.v.display('VEND FAIL')
455 elif (exitcode == 5): # RV_BALANCE
456 self.v.display('NO MONEY?')
457 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
458 self.v.display('EMPTY SLOT')
459 elif (exitcode == 1): # RV_BADITEM (Dead slot)
460 self.v.display('EMPTY SLOT')
462 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))
463 self.v.display('UNK ERROR')
467 Triggered when the user presses a button while entering their pin.
469 def handle_getting_pin_key(self, event, params):
471 if len(self.vstatus.cur_pin) < PIN_LENGTH:
473 if self.vstatus.cur_pin == '':
474 self.vstatus.cur_user = ''
475 self.dispense.logOut()
479 self.vstatus.cur_pin = ''
480 self.vstatus.mk.set_message('PIN: ')
482 self.vstatus.cur_pin += chr(key + ord('0'))
483 self.vstatus.mk.set_message('PIN: '+'X'*len(self.vstatus.cur_pin))
484 if len(self.vstatus.cur_pin) == PIN_LENGTH:
485 if self.dispense.authUserIdPin(self.vstatus.cur_user, self.vstatus.cur_pin):
486 self.vstatus.username = self.dispense.getUsername()
487 self.v.beep(0, False)
488 self.vstatus.cur_selection = ''
489 self.vstatus.change_state(STATE_GET_SELECTION)
490 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
493 self.v.beep(40, False)
494 self.vstatus.mk.set_messages(
495 [(self.center('BAD PIN'), False, 1.0),
496 (self.center('SORRY'), False, 0.5)])
497 self.vstatus.cur_user = ''
498 self.vstatus.cur_pin = ''
505 Triggered when the user presses a button while entering their user id.
507 def handle_getting_uid_key(self, event, params):
509 # complicated key handling here:
511 if len(self.vstatus.cur_user) == 0 and key == 9:
512 self.vstatus.cur_selection = ''
513 self.vstatus.time_to_autologout = None
514 self.vstatus.mk.set_message('PRICECHECK')
516 self.scroll_options('', self.vstatus.mk)
517 self.vstatus.change_state(STATE_GET_SELECTION)
520 if len(self.vstatus.cur_user) <8:
522 self.vstatus.cur_user = ''
523 self.dispense.logOut()
527 self.vstatus.cur_user += chr(key + ord('0'))
528 #logging.info('dob: '+vstatus.cur_user)
529 if len(self.vstatus.cur_user) > 5:
530 self.vstatus.mk.set_message('>'+self.vstatus.cur_user)
532 self.vstatus.mk.set_message('UID: '+self.vstatus.cur_user)
534 if len(self.vstatus.cur_user) == 5:
535 uid = int(self.vstatus.cur_user)
538 logging.info('user '+self.vstatus.cur_user+' has a bad PIN')
544 Welcome to Picklevision Sytems, Sunnyvale, CA
546 Greetings Professor Falken.
551 Shall we play a game?
554 Please choose from the following menu:
561 6. Toxic and Biochemical Warfare
562 7. Global Thermonuclear War
566 Wouldn't you prefer a nice game of chess?
568 """.replace('\n',' ')
569 self.vstatus.mk.set_messages([(pfalken, False, 10)])
570 self.vstatus.cur_user = ''
571 self.vstatus.cur_pin = ''
577 # TODO Fix this up do we can check before logging in
579 if self.dispense.isDisabled():
580 logging.info('user '+self.vstatus.cur_user+' is disabled')
581 self.vstatus.mk.set_messages(
582 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
583 self.vstatus.cur_user = ''
584 self.vstatus.cur_pin = ''
590 self.vstatus.cur_pin = ''
591 self.vstatus.mk.set_message('PIN: ')
592 logging.info('need pin for user %s'%self.vstatus.cur_user)
593 self.vstatus.change_state(STATE_GETTING_PIN)
597 Triggered when a key is pressed and the machine is idling.
599 def handle_idle_key(self, event, params):
602 self.vstatus.cur_user = ''
603 self.dispense.logOut()
607 self.vstatus.change_state(STATE_GETTING_UID)
608 self.run_handler(event, params)
611 What to do when there is nothing to do.
613 def handle_idle_tick(self, event, params):
615 if self.vstatus.mk.done():
618 if self.vstatus.time_of_next_idler and time() > self.vstatus.time_of_next_idler:
619 self.vstatus.time_of_next_idler = time() + 30
624 self.vstatus.mk.update_display()
626 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK)
627 self.run_handler(event, params)
631 Manages the beeps for the grandfather clock
633 def beep_on(self, when, before=0):
634 start = int(when - before)
638 if now >= start and now <= end:
642 def handle_idle_grandfather_tick(self, event, params):
643 ### check for interesting times
646 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
647 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
648 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
649 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
651 hourfromnow = localtime(time() + 3600)
653 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
654 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
655 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
657 ## check for X seconds to the hour
658 ## if case, update counter to 2
659 if self.beep_on(onthehour,15) \
660 or self.beep_on(halfhour,0) \
661 or self.beep_on(quarterhour,0) \
662 or self.beep_on(threequarterhour,0) \
663 or self.beep_on(fivetothehour,0):
664 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
665 self.run_handler(event, params)
667 self.vstatus.change_state(STATE_IDLE)
669 def handle_grandfather_tick(self, event, params):
673 ### we live in interesting times
676 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
677 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
678 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
679 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
681 hourfromnow = localtime(time() + 3600)
683 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
684 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
685 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
688 #print "when it fashionable to wear a onion on your hip"
690 if self.beep_on(onthehour,15):
692 next_hour=((hourfromnow[3] + 11) % 12) + 1
693 if onthehour - time() < next_hour and onthehour - time() > 0:
694 self.v.beep(0, False)
698 msg.append(("DING!", False, None))
700 msg.append((" DING!", False, None))
701 elif int(onthehour - time()) == 0:
702 self.v.beep(255, False)
703 msg.append((" BONG!", False, None))
704 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
705 elif self.beep_on(halfhour,0):
707 self.v.beep(0, False)
708 msg.append((" HALFHOUR ", False, 50))
709 elif self.beep_on(quarterhour,0):
711 self.v.beep(0, False)
712 msg.append((" QTR HOUR ", False, 50))
713 elif self.beep_on(threequarterhour,0):
715 self.v.beep(0, False)
716 msg.append((" 3 QTR HR ", False, 50))
717 elif self.beep_on(fivetothehour,0):
719 self.v.beep(0, False)
720 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
724 ## check for X seconds to the hour
727 self.vstatus.mk.set_messages(msg)
730 self.vstatus.mk.update_display()
731 ## if no longer case, return to idle
733 ## change idler to be clock
734 if go_idle and self.vstatus.mk.done():
735 self.vstatus.change_state(STATE_IDLE,1)
738 What to do when the door is open.
740 def handle_door_idle(self, event, params):
741 def twiddle(clock,v,wise = 2):
743 v.display("-FEED ME-")
744 elif (clock % 4 == 1+wise):
745 v.display("\\FEED ME/")
746 elif (clock % 4 == 2):
747 v.display("-FEED ME-")
748 elif (clock % 4 == 3-wise):
749 v.display("/FEED ME\\")
751 # don't care right now.
754 if ((now % 60 % 2) == 0):
757 twiddle(now, self.v, wise=0)
760 What to do when the door is opened or closed.
762 def handle_door_event(self, event, params):
763 if params == 0: #door open
764 self.vstatus.change_state(STATE_DOOR_OPENING)
765 logging.warning("Entering open door mode")
766 self.v.display("-FEED ME-")
768 self.dispense.logOut()
769 self.vstatus.cur_user = ''
770 self.vstatus.cur_pin = ''
771 elif params == 1: #door closed
772 self.vstatus.change_state(STATE_DOOR_CLOSING)
775 logging.warning('Leaving open door mode')
776 self.v.display("-YUM YUM!-")
779 Triggered when a user swipes their caed, and the machine is logged out.
781 def handle_mifare_event(self, event, params):
783 # Translate card_id into uid.
784 if card_id == None or card_id == self._last_card_id:
787 self._last_card_id = card_id
789 if not self.dispense.authMifareCard(card_id):
790 self.v.beep(40, False)
791 self.vstatus.mk.set_messages(
792 [(self.center('BAD CARD'), False, 1.0),
793 (self.center('SORRY'), False, 0.5)])
794 self.vstatus.cur_user = ''
795 self.vstatus.cur_pin = ''
796 self._last_card_id = -1
800 elif self.dispense.isDisabled():
801 logging.info('Mapped card id to uid %s'%self.dispense.getUsername())
802 self.v.beep(40, False)
803 self.vstatus.mk.set_messages(
804 [(self.center('ACCT DISABLED'), False, 1.0),
805 (self.center('SORRY'), False, 0.5)])
806 self.dispense.logOut()
810 logging.info('Mapped card id to uid %s'%self.dispense.getUsername())
811 self.vstatus.cur_user = '----'
812 self.vstatus.username = self.dispense.getUsername()
813 self.vstatus.cur_selection = ''
814 self.vstatus.change_state(STATE_GET_SELECTION)
815 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
819 Triggered when a user swipes their card and the machine is logged in.
821 def handle_mifare_add_user_event(self, event, params):
824 # Translate card_id into uid.
825 if card_id == None or card_id == self._last_card_id:
828 self._last_card_id = card_id
830 if not self.dispense.addCard(card_id):
831 self.vstatus.mk.set_messages(
832 [(self.center('ALREADY'), False, 0.5),
833 (self.center('ENROLLED'), False, 0.5)])
835 self.vstatus.mk.set_messages(
836 [(self.center('CARD'), False, 0.5),
837 (self.center('ENROLLED'), False, 0.5)])
839 def return_to_idle(self, event, params):
843 Maps what to do when the state changes.
845 def create_state_table(self):
846 self.vstatus.state_table[(STATE_IDLE,TICK,1)] = self.handle_idle_tick
847 self.vstatus.state_table[(STATE_IDLE,KEY,1)] = self.handle_idle_key
848 self.vstatus.state_table[(STATE_IDLE,DOOR,1)] = self.handle_door_event
849 self.vstatus.state_table[(STATE_IDLE,MIFARE,1)] = self.handle_mifare_event
851 self.vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = self.handle_door_idle
852 self.vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = self.handle_door_event
853 self.vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = self.do_nothing
854 self.vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = self.do_nothing
856 self.vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = self.return_to_idle
857 self.vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = self.handle_door_event
858 self.vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = self.do_nothing
859 self.vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = self.do_nothing
861 self.vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = self.handle_getting_uid_idle
862 self.vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = self.handle_door_event
863 self.vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = self.handle_getting_uid_key
864 self.vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = self.handle_mifare_event
866 self.vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = self.handle_getting_pin_idle
867 self.vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = self.handle_door_event
868 self.vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = self.handle_getting_pin_key
869 self.vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = self.handle_mifare_event
871 self.vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = self.handle_get_selection_idle
872 self.vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = self.handle_door_event
873 self.vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = self.handle_get_selection_key
874 self.vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = self.handle_mifare_add_user_event
876 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = self.handle_idle_grandfather_tick
877 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = self.handle_grandfather_tick
878 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = self.handle_door_event
879 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = self.handle_door_event
880 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = self.do_nothing
881 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = self.do_nothing
882 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = self.handle_mifare_event
885 Get what to do on a state change.
887 def get_state_table_handler(self, state, event, counter):
888 return self.vstatus.state_table[(state,event,counter)]
890 def time_to_next_update(self):
891 idle_update = self.vstatus.time_of_next_idlestep - time()
892 if not self.vstatus.mk.done() and self.vstatus.mk.next_update is not None:
893 mk_update = self.vstatus.mk.next_update - time()
894 if mk_update < idle_update:
895 idle_update = mk_update
898 def run_forever(self, rfh, wfh, options, cf):
899 self.v = VendingMachine(rfh, wfh, USE_MIFARE)
900 self.dispense = Dispense()
901 self.vstatus = VendState(self.v)
902 self.create_state_table()
904 logging.debug('PING is ' + str(self.v.ping()))
910 timeout = self.time_to_next_update()
911 (event, params) = self.v.next_event(timeout)
912 self.run_handler(event, params)
914 def run_handler(self, event, params):
915 handler = self.get_state_table_handler(self.vstatus.state,event,self.vstatus.counter)
917 handler(event, params)
920 Connect to the machine.
922 def connect_to_vend(options, cf):
925 logging.info('Connecting to vending machine using LAT')
926 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
927 rfh, wfh = latclient.get_fh()
928 elif options.use_serial:
929 # Open vending machine via serial.
930 logging.info('Connecting to vending machine using serial')
931 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
932 rfh,wfh = serialclient.get_fh()
934 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
935 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
937 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
938 sock.connect((options.host, options.port))
939 rfh = sock.makefile('r')
940 wfh = sock.makefile('w')
947 Parse arguments from the command line
950 from optparse import OptionParser
952 op = OptionParser(usage="%prog [OPTION]...")
953 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')
954 op.add_option('--serial', action='store_true', default=False, dest='use_serial', help='use the serial port')
955 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
956 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
957 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
958 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
959 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
960 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
961 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
962 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
963 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
964 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
965 options, args = op.parse_args()
968 op.error('extra command line arguments: ' + ' '.join(args))
972 def create_pid_file(name):
974 pid_file = file(name, 'w')
975 pid_file.write('%d\n'%os.getpid())
978 logging.warning('unable to write to pid file '+name+': '+str(e))
981 def do_nothing(signum, stack):
982 signal.signal(signum, do_nothing)
983 def stop_server(signum, stack): raise KeyboardInterrupt
984 signal.signal(signal.SIGHUP, do_nothing)
985 signal.signal(signal.SIGTERM, stop_server)
986 signal.signal(signal.SIGINT, stop_server)
988 options = parse_args()
989 config_opts = VendConfigFile(options.config_file, config_options)
990 if options.daemon: become_daemon()
991 set_up_logging(options)
992 if options.pid_file != '': create_pid_file(options.pid_file)
994 return options, config_opts
996 def clean_up_nicely(options, config_opts):
997 if options.pid_file != '':
999 os.unlink(options.pid_file)
1000 logging.debug('Removed pid file '+options.pid_file)
1001 except OSError: pass # if we can't delete it, meh
1003 def set_up_logging(options):
1004 logger = logging.getLogger()
1006 if not options.daemon:
1007 stderr_logger = logging.StreamHandler(sys.stderr)
1008 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1009 logger.addHandler(stderr_logger)
1011 if options.log_file != '':
1013 file_logger = logging.FileHandler(options.log_file)
1014 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1015 logger.addHandler(file_logger)
1017 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1019 if options.syslog != None:
1020 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1021 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1022 logger.addHandler(sys_logger)
1025 logger.setLevel(logging.WARNING)
1026 elif options.verbose:
1027 logger.setLevel(logging.DEBUG)
1029 logger.setLevel(logging.INFO)
1031 def become_daemon():
1032 dev_null = file('/dev/null')
1033 fd = dev_null.fileno()
1042 raise SystemExit('failed to fork: '+str(e))
1044 def do_vend_server(options, config_opts):
1047 rfh, wfh = connect_to_vend(options, config_opts)
1048 except (SerialClientException, socket.error), e:
1049 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1051 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1052 logging.info("Trying again in 5 seconds.")
1056 # run_forever(rfh, wfh, options, config_opts)
1059 vserver = VendServer()
1060 vserver.run_forever(rfh, wfh, options, config_opts)
1061 except VendingException:
1062 logging.error("Connection died, trying again...")
1063 logging.info("Trying again in 5 seconds.")
1067 def main(argv=None):
1068 options, config_opts = set_stuff_up()
1071 logging.warning('Starting Vend Server')
1072 do_vend_server(options, config_opts)
1073 logging.error('Vend Server finished unexpectedly, restarting')
1074 except KeyboardInterrupt:
1075 logging.info("Killed by signal, cleaning up")
1076 clean_up_nicely(options, config_opts)
1077 logging.warning("Vend Server stopped")
1082 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1083 tb = format_tb(exc_traceback, 20)
1086 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1087 logging.critical("Message: " + str(exc_value))
1088 logging.critical("Traceback:")
1090 for line in event.split('\n'):
1091 logging.critical(' '+line)
1092 logging.critical("This message should be considered a bug in the Vend Server.")
1093 logging.critical("Please report this to someone who can fix it.")
1095 logging.warning("Trying again anyway (might not help, but hey...)")
1097 if __name__ == '__main__':