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()
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 if self.vstatus.time_to_autologout != None and self.vstatus.time_to_autologout - time() <= 0:
339 self.vstatus.time_to_autologout = None
340 self.vstatus.cur_user = ''
341 self.vstatus.cur_pin = ''
342 self.vstatus.cur_selection = ''
343 self._last_card_id = -1
346 ### State fully logged out ... reset variables
347 if self.vstatus.time_to_autologout and not self.vstatus.mk.done():
348 self.vstatus.time_to_autologout = None
349 if self.vstatus.cur_user == '' and self.vstatus.time_to_autologout:
350 self.vstatus.time_to_autologout = None
353 if len(self.vstatus.cur_pin) == PIN_LENGTH and self.vstatus.mk.done() and self.vstatus.time_to_autologout == None:
355 self.vstatus.time_to_autologout = time() + 15
356 self.vstatus.last_timeout_refresh = None
358 ## FIXME - this may need to be elsewhere.....
360 self.vstatus.mk.update_display()
363 Triggered on user input while logged in.
365 def handle_get_selection_key(self, event, params):
367 if len(self.vstatus.cur_selection) == 0:
369 self.vstatus.cur_pin = ''
370 self.vstatus.cur_user = ''
371 self.vstatus.cur_selection = ''
373 self.vstatus.mk.set_messages([(self.center('BYE!'), False, 1.5)])
376 self.vstatus.cur_selection += chr(key + ord('0'))
377 self.vstatus.mk.set_message('SELECT: '+self.vstatus.cur_selection)
378 self.vstatus.time_to_autologout = None
379 elif len(self.vstatus.cur_selection) == 1:
381 self.vstatus.cur_selection = ''
382 self.vstatus.time_to_autologout = None
383 self.scroll_options(self.vstatus.username, self.vstatus.mk)
386 self.vstatus.cur_selection += chr(key + ord('0'))
387 if self.vstatus.cur_user:
388 self.make_selection()
389 self.vstatus.cur_selection = ''
390 self.vstatus.time_to_autologout = time() + 8
391 self.vstatus.last_timeout_refresh = None
394 self.dispense.getItemInfo(self.vstatus.cur_selection)
395 self.vstatus.cur_selection = ''
396 self.vstatus.time_to_autologout = None
397 self.vstatus.last_timeout_refresh = None
400 Triggered when the user has entered the id of something they would like to purchase.
402 def make_selection(self):
403 # should use sudo here
404 if self.vstatus.cur_selection == '55':
405 self.vstatus.mk.set_message('OPENSESAME')
406 logging.info('dispensing a door for %s'%self.vstatus.username)
408 ret = os.system('dispense -u "%s" door'%self.vstatus.username)
410 ret = os.system('dispense door')
412 logging.info('door opened')
413 self.vstatus.mk.set_message(self.center('DOOR OPEN'))
415 logging.warning('user %s tried to dispense a bad door'%self.vstatus.username)
416 self.vstatus.mk.set_message(self.center('BAD DOOR'))
418 elif self.vstatus.cur_selection == '81':
420 elif self.vstatus.cur_selection == '99':
421 self.scroll_options(self.vstatus.username, self.vstatus.mk)
422 self.vstatus.cur_selection = ''
424 elif self.vstatus.cur_selection[1] == '8':
425 self.v.display('GOT DRINK?')
426 if ((os.system('dispense -u "%s" coke:%s'%(self.vstatus.username, self.vstatus.cur_selection[0])) >> 8) != 0):
427 self.v.display('SEEMS NOT')
429 self.v.display('GOT DRINK!')
431 # first see if it's a named slot
433 price, shortname, name = get_snack( self.vstatus.cur_selection )
435 price, shortname, name = get_snack( '--' )
436 dollarprice = "$%.2f" % ( price / 100.0 )
437 self.v.display(self.vstatus.cur_selection+' - %s'%dollarprice)
438 exitcode = os.system('dispense -u "%s" snack:%s'%(self.vstatus.username, self.vstatus.cur_selection)) >> 8
440 # magic dispense syslog service
441 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, self.vstatus.cur_selection, self.vstatus.username))
442 (worked, code, string) = self.v.vend(self.vstatus.cur_selection)
444 self.v.display('THANK YOU')
446 print "Vend Failed:", code, string
447 self.v.display('VEND FAIL')
448 elif (exitcode == 5): # RV_BALANCE
449 self.v.display('NO MONEY?')
450 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
451 self.v.display('EMPTY SLOT')
452 elif (exitcode == 1): # RV_BADITEM (Dead slot)
453 self.v.display('EMPTY SLOT')
455 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))
456 self.v.display('UNK ERROR')
460 Triggered when the user presses a button while entering their pin.
462 def handle_getting_pin_key(self, event, params):
464 if len(self.vstatus.cur_pin) < PIN_LENGTH:
466 if self.vstatus.cur_pin == '':
467 self.vstatus.cur_user = ''
471 self.vstatus.cur_pin = ''
472 self.vstatus.mk.set_message('PIN: ')
474 self.vstatus.cur_pin += chr(key + ord('0'))
475 self.vstatus.mk.set_message('PIN: '+'X'*len(self.vstatus.cur_pin))
476 if len(self.vstatus.cur_pin) == PIN_LENGTH:
477 self.dispense.authUserIdPin(self.vstatus.cur_user, self.vstatus.cur_pin)
478 if self.dispense.getUsername():
479 self.v.beep(0, False)
480 self.vstatus.cur_selection = ''
481 self.vstatus.change_state(STATE_GET_SELECTION)
482 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
485 self.v.beep(40, False)
486 self.vstatus.mk.set_messages(
487 [(self.center('BAD PIN'), False, 1.0),
488 (self.center('SORRY'), False, 0.5)])
489 self.vstatus.cur_user = ''
490 self.vstatus.cur_pin = ''
497 Triggered when the user presses a button while entering their user id.
499 def handle_getting_uid_key(self, event, params):
501 # complicated key handling here:
503 if len(self.vstatus.cur_user) == 0 and key == 9:
504 self.vstatus.cur_selection = ''
505 self.vstatus.time_to_autologout = None
506 self.vstatus.mk.set_message('PRICECHECK')
508 self.scroll_options('', self.vstatus.mk)
509 self.vstatus.change_state(STATE_GET_SELECTION)
512 if len(self.vstatus.cur_user) <8:
514 self.vstatus.cur_user = ''
518 self.vstatus.cur_user += chr(key + ord('0'))
519 #logging.info('dob: '+vstatus.cur_user)
520 if len(self.vstatus.cur_user) > 5:
521 self.vstatus.mk.set_message('>'+self.vstatus.cur_user)
523 self.vstatus.mk.set_message('UID: '+self.vstatus.cur_user)
525 if len(self.vstatus.cur_user) == 5:
526 uid = int(self.vstatus.cur_user)
529 logging.info('user '+self.vstatus.cur_user+' has a bad PIN')
535 Welcome to Picklevision Sytems, Sunnyvale, CA
537 Greetings Professor Falken.
542 Shall we play a game?
545 Please choose from the following menu:
552 6. Toxic and Biochemical Warfare
553 7. Global Thermonuclear War
557 Wouldn't you prefer a nice game of chess?
559 """.replace('\n',' ')
560 self.vstatus.mk.set_messages([(pfalken, False, 10)])
561 self.vstatus.cur_user = ''
562 self.vstatus.cur_pin = ''
568 # TODO Fix this up do we can check before logging in
570 if self.dispense.isDisabled():
571 logging.info('user '+self.vstatus.cur_user+' is disabled')
572 self.vstatus.mk.set_messages(
573 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
574 self.vstatus.cur_user = ''
575 self.vstatus.cur_pin = ''
581 self.vstatus.cur_pin = ''
582 self.vstatus.mk.set_message('PIN: ')
583 logging.info('need pin for user %s'%self.vstatus.cur_user)
584 self.vstatus.change_state(STATE_GETTING_PIN)
588 Triggered when a key is pressed and the machine is idling.
590 def handle_idle_key(self, event, params):
593 self.vstatus.cur_user = ''
597 self.vstatus.change_state(STATE_GETTING_UID)
598 self.run_handler(event, params)
601 What to do when there is nothing to do.
603 def handle_idle_tick(self, event, params):
605 if self.vstatus.mk.done():
608 if self.vstatus.time_of_next_idler and time() > self.vstatus.time_of_next_idler:
609 self.vstatus.time_of_next_idler = time() + 30
614 self.vstatus.mk.update_display()
616 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK)
617 self.run_handler(event, params)
621 Manages the beeps for the grandfather clock
623 def beep_on(self, when, before=0):
624 start = int(when - before)
628 if now >= start and now <= end:
632 def handle_idle_grandfather_tick(self, event, params):
633 ### check for interesting times
636 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
637 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
638 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
639 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
641 hourfromnow = localtime(time() + 3600)
643 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
644 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
645 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
647 ## check for X seconds to the hour
648 ## if case, update counter to 2
649 if self.beep_on(onthehour,15) \
650 or self.beep_on(halfhour,0) \
651 or self.beep_on(quarterhour,0) \
652 or self.beep_on(threequarterhour,0) \
653 or self.beep_on(fivetothehour,0):
654 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
655 self.run_handler(event, params)
657 self.vstatus.change_state(STATE_IDLE)
659 def handle_grandfather_tick(self, event, params):
663 ### we live in interesting times
666 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
667 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
668 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
669 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
671 hourfromnow = localtime(time() + 3600)
673 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
674 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
675 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
678 #print "when it fashionable to wear a onion on your hip"
680 if self.beep_on(onthehour,15):
682 next_hour=((hourfromnow[3] + 11) % 12) + 1
683 if onthehour - time() < next_hour and onthehour - time() > 0:
684 self.v.beep(0, False)
688 msg.append(("DING!", False, None))
690 msg.append((" DING!", False, None))
691 elif int(onthehour - time()) == 0:
692 self.v.beep(255, False)
693 msg.append((" BONG!", False, None))
694 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
695 elif self.beep_on(halfhour,0):
697 self.v.beep(0, False)
698 msg.append((" HALFHOUR ", False, 50))
699 elif self.beep_on(quarterhour,0):
701 self.v.beep(0, False)
702 msg.append((" QTR HOUR ", False, 50))
703 elif self.beep_on(threequarterhour,0):
705 self.v.beep(0, False)
706 msg.append((" 3 QTR HR ", False, 50))
707 elif self.beep_on(fivetothehour,0):
709 self.v.beep(0, False)
710 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
714 ## check for X seconds to the hour
717 self.vstatus.mk.set_messages(msg)
720 self.vstatus.mk.update_display()
721 ## if no longer case, return to idle
723 ## change idler to be clock
724 if go_idle and self.vstatus.mk.done():
725 self.vstatus.change_state(STATE_IDLE,1)
728 What to do when the door is open.
730 def handle_door_idle(self, event, params):
731 def twiddle(clock,v,wise = 2):
733 v.display("-FEED ME-")
734 elif (clock % 4 == 1+wise):
735 v.display("\\FEED ME/")
736 elif (clock % 4 == 2):
737 v.display("-FEED ME-")
738 elif (clock % 4 == 3-wise):
739 v.display("/FEED ME\\")
741 # don't care right now.
744 if ((now % 60 % 2) == 0):
747 twiddle(now, self.v, wise=0)
750 What to do when the door is opened or closed.
752 def handle_door_event(self, event, params):
753 if params == 0: #door open
754 self.vstatus.change_state(STATE_DOOR_OPENING)
755 logging.warning("Entering open door mode")
756 self.v.display("-FEED ME-")
758 self.vstatus.cur_user = ''
759 self.vstatus.cur_pin = ''
760 elif params == 1: #door closed
761 self.vstatus.change_state(STATE_DOOR_CLOSING)
764 logging.warning('Leaving open door mode')
765 self.v.display("-YUM YUM!-")
768 Triggered when a user swipes their caed, and the machine is logged out.
770 def handle_mifare_event(self, event, params):
772 # Translate card_id into uid.
773 if card_id == None or card_id == self._last_card_id:
776 self._last_card_id = card_id
778 self.dispense.authMifareCard(card_id)
779 logging.info('Mapped card id to uid %s'%self.dispense.getUsername())
780 if not self.dispense.isLoggedIn():
781 self.v.beep(40, False)
782 self.vstatus.mk.set_messages(
783 [(self.center('BAD CARD'), False, 1.0),
784 (self.center('SORRY'), False, 0.5)])
785 self.vstatus.cur_user = ''
786 self.vstatus.cur_pin = ''
787 self._last_card_id = -1
791 elif self.dispense.isDisabled():
792 self.v.beep(40, False)
793 self.vstatus.mk.set_messages(
794 [(self.center('ACCT DISABLED'), False, 1.0),
795 (self.center('SORRY'), False, 0.5)])
796 self.dispense.logOut()
800 self.vstatus.cur_selection = ''
801 self.vstatus.change_state(STATE_GET_SELECTION)
802 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
806 Triggered when a user swipes their card and the machine is logged in.
808 def handle_mifare_add_user_event(self, event, params):
811 # Translate card_id into uid.
812 if card_id == None or card_id == self._last_card_id:
815 self._last_card_id = card_id
817 if not self.dispense.addCard(card_id):
818 self.vstatus.mk.set_messages(
819 [(self.center('ALREADY'), False, 0.5),
820 (self.center('ENROLLED'), False, 0.5)])
822 self.vstatus.mk.set_messages(
823 [(self.center('CARD'), False, 0.5),
824 (self.center('ENROLLED'), False, 0.5)])
826 def return_to_idle(self, event, params):
830 Maps what to do when the state changes.
832 def create_state_table(self):
833 self.vstatus.state_table[(STATE_IDLE,TICK,1)] = self.handle_idle_tick
834 self.vstatus.state_table[(STATE_IDLE,KEY,1)] = self.handle_idle_key
835 self.vstatus.state_table[(STATE_IDLE,DOOR,1)] = self.handle_door_event
836 self.vstatus.state_table[(STATE_IDLE,MIFARE,1)] = self.handle_mifare_event
838 self.vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = self.handle_door_idle
839 self.vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = self.handle_door_event
840 self.vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = self.do_nothing
841 self.vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = self.do_nothing
843 self.vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = self.return_to_idle
844 self.vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = self.handle_door_event
845 self.vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = self.do_nothing
846 self.vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = self.do_nothing
848 self.vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = self.handle_getting_uid_idle
849 self.vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = self.handle_door_event
850 self.vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = self.handle_getting_uid_key
851 self.vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = self.handle_mifare_event
853 self.vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = self.handle_getting_pin_idle
854 self.vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = self.handle_door_event
855 self.vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = self.handle_getting_pin_key
856 self.vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = self.handle_mifare_event
858 self.vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = self.handle_get_selection_idle
859 self.vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = self.handle_door_event
860 self.vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = self.handle_get_selection_key
861 self.vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = self.handle_mifare_add_user_event
863 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = self.handle_idle_grandfather_tick
864 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = self.handle_grandfather_tick
865 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = self.handle_door_event
866 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = self.handle_door_event
867 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = self.do_nothing
868 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = self.do_nothing
869 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = self.handle_mifare_event
872 Get what to do on a state change.
874 def get_state_table_handler(self, state, event, counter):
875 return self.vstatus.state_table[(state,event,counter)]
877 def time_to_next_update(self):
878 idle_update = self.vstatus.time_of_next_idlestep - time()
879 if not self.vstatus.mk.done() and self.vstatus.mk.next_update is not None:
880 mk_update = self.vstatus.mk.next_update - time()
881 if mk_update < idle_update:
882 idle_update = mk_update
885 def run_forever(self, rfh, wfh, options, cf):
886 self.v = VendingMachine(rfh, wfh, USE_MIFARE)
887 self.dispense = Dispense()
888 self.vstatus = VendState(self.v)
889 self.create_state_table()
891 logging.debug('PING is ' + str(self.v.ping()))
897 timeout = self.time_to_next_update()
898 (event, params) = self.v.next_event(timeout)
899 self.run_handler(event, params)
901 def run_handler(self, event, params):
902 handler = self.get_state_table_handler(self.vstatus.state,event,self.vstatus.counter)
904 handler(event, params)
907 Connect to the machine.
909 def connect_to_vend(options, cf):
912 logging.info('Connecting to vending machine using LAT')
913 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
914 rfh, wfh = latclient.get_fh()
915 elif options.use_serial:
916 # Open vending machine via serial.
917 logging.info('Connecting to vending machine using serial')
918 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
919 rfh,wfh = serialclient.get_fh()
921 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
922 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
924 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
925 sock.connect((options.host, options.port))
926 rfh = sock.makefile('r')
927 wfh = sock.makefile('w')
934 Parse arguments from the command line
937 from optparse import OptionParser
939 op = OptionParser(usage="%prog [OPTION]...")
940 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')
941 op.add_option('--serial', action='store_true', default=False, dest='use_serial', help='use the serial port')
942 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
943 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
944 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
945 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
946 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
947 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
948 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
949 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
950 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
951 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
952 options, args = op.parse_args()
955 op.error('extra command line arguments: ' + ' '.join(args))
959 def create_pid_file(name):
961 pid_file = file(name, 'w')
962 pid_file.write('%d\n'%os.getpid())
965 logging.warning('unable to write to pid file '+name+': '+str(e))
968 def do_nothing(signum, stack):
969 signal.signal(signum, do_nothing)
970 def stop_server(signum, stack): raise KeyboardInterrupt
971 signal.signal(signal.SIGHUP, do_nothing)
972 signal.signal(signal.SIGTERM, stop_server)
973 signal.signal(signal.SIGINT, stop_server)
975 options = parse_args()
976 config_opts = VendConfigFile(options.config_file, config_options)
977 if options.daemon: become_daemon()
978 set_up_logging(options)
979 if options.pid_file != '': create_pid_file(options.pid_file)
981 return options, config_opts
983 def clean_up_nicely(options, config_opts):
984 if options.pid_file != '':
986 os.unlink(options.pid_file)
987 logging.debug('Removed pid file '+options.pid_file)
988 except OSError: pass # if we can't delete it, meh
990 def set_up_logging(options):
991 logger = logging.getLogger()
993 if not options.daemon:
994 stderr_logger = logging.StreamHandler(sys.stderr)
995 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
996 logger.addHandler(stderr_logger)
998 if options.log_file != '':
1000 file_logger = logging.FileHandler(options.log_file)
1001 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1002 logger.addHandler(file_logger)
1004 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1006 if options.syslog != None:
1007 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1008 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1009 logger.addHandler(sys_logger)
1012 logger.setLevel(logging.WARNING)
1013 elif options.verbose:
1014 logger.setLevel(logging.DEBUG)
1016 logger.setLevel(logging.INFO)
1018 def become_daemon():
1019 dev_null = file('/dev/null')
1020 fd = dev_null.fileno()
1029 raise SystemExit('failed to fork: '+str(e))
1031 def do_vend_server(options, config_opts):
1034 rfh, wfh = connect_to_vend(options, config_opts)
1035 except (SerialClientException, socket.error), e:
1036 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1038 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1039 logging.info("Trying again in 5 seconds.")
1043 # run_forever(rfh, wfh, options, config_opts)
1046 vserver = VendServer()
1047 vserver.run_forever(rfh, wfh, options, config_opts)
1048 except VendingException:
1049 logging.error("Connection died, trying again...")
1050 logging.info("Trying again in 5 seconds.")
1054 def main(argv=None):
1055 options, config_opts = set_stuff_up()
1058 logging.warning('Starting Vend Server')
1059 do_vend_server(options, config_opts)
1060 logging.error('Vend Server finished unexpectedly, restarting')
1061 except KeyboardInterrupt:
1062 logging.info("Killed by signal, cleaning up")
1063 clean_up_nicely(options, config_opts)
1064 logging.warning("Vend Server stopped")
1069 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1070 tb = format_tb(exc_traceback, 20)
1073 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1074 logging.critical("Message: " + str(exc_value))
1075 logging.critical("Traceback:")
1077 for line in event.split('\n'):
1078 logging.critical(' '+line)
1079 logging.critical("This message should be considered a bug in the Vend Server.")
1080 logging.critical("Please report this to someone who can fix it.")
1082 logging.warning("Trying again anyway (might not help, but hey...)")
1084 if __name__ == '__main__':