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 = ''
374 self.vstatus.mk.set_messages([(self.center('BYE!'), False, 1.5)])
377 self.vstatus.cur_selection += chr(key + ord('0'))
378 self.vstatus.mk.set_message('SELECT: '+self.vstatus.cur_selection)
379 self.vstatus.time_to_autologout = None
380 elif len(self.vstatus.cur_selection) == 1:
382 self.vstatus.cur_selection = ''
383 self.vstatus.time_to_autologout = None
384 self.scroll_options(self.vstatus.username, self.vstatus.mk)
387 self.vstatus.cur_selection += chr(key + ord('0'))
388 if self.vstatus.cur_user:
389 self.make_selection()
390 self.vstatus.cur_selection = ''
391 self.vstatus.time_to_autologout = time() + 8
392 self.vstatus.last_timeout_refresh = None
395 (name,price) = self.dispense.getItemInfo(self.vstatus.cur_selection)
396 dollarprice = "$%.2f" % ( price / 100.0 )
397 dollarprice = dollarprice[:-4] + '.' + dollarprice[-4] + dollarprice[-2:] # Work around display bug
398 self.v.display( self.vstatus.cur_selection+' - %s'%dollarprice)
400 self.vstatus.cur_selection = ''
401 self.vstatus.time_to_autologout = None
402 self.vstatus.last_timeout_refresh = None
405 Triggered when the user has entered the id of something they would like to purchase.
407 def make_selection(self):
408 # should use sudo here
409 if self.vstatus.cur_selection == '55':
410 self.vstatus.mk.set_message('OPENSESAME')
411 logging.info('dispensing a door for %s'%self.vstatus.username)
413 ret = os.system('dispense -u "%s" door'%self.vstatus.username)
415 ret = os.system('dispense door')
417 logging.info('door opened')
418 self.vstatus.mk.set_message(self.center('DOOR OPEN'))
420 logging.warning('user %s tried to dispense a bad door'%self.vstatus.username)
421 self.vstatus.mk.set_message(self.center('BAD DOOR'))
423 elif self.vstatus.cur_selection == '81':
425 elif self.vstatus.cur_selection == '99':
426 self.scroll_options(self.vstatus.username, self.vstatus.mk)
427 self.vstatus.cur_selection = ''
429 elif self.vstatus.cur_selection[1] == '8':
430 self.v.display('GOT DRINK?')
431 if ((os.system('dispense -u "%s" coke:%s'%(self.vstatus.username, self.vstatus.cur_selection[0])) >> 8) != 0):
432 self.v.display('SEEMS NOT')
434 self.v.display('GOT DRINK!')
436 # first see if it's a named slot
438 price, shortname, name = get_snack( self.vstatus.cur_selection )
440 price, shortname, name = get_snack( '--' )
441 dollarprice = "$%.2f" % ( price / 100.0 )
442 self.v.display(self.vstatus.cur_selection+' - %s'%dollarprice)
443 exitcode = os.system('dispense -u "%s" snack:%s'%(self.vstatus.username, self.vstatus.cur_selection)) >> 8
445 # magic dispense syslog service
446 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, self.vstatus.cur_selection, self.vstatus.username))
447 (worked, code, string) = self.v.vend(self.vstatus.cur_selection)
449 self.v.display('THANK YOU')
451 print "Vend Failed:", code, string
452 self.v.display('VEND FAIL')
453 elif (exitcode == 5): # RV_BALANCE
454 self.v.display('NO MONEY?')
455 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
456 self.v.display('EMPTY SLOT')
457 elif (exitcode == 1): # RV_BADITEM (Dead slot)
458 self.v.display('EMPTY SLOT')
460 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))
461 self.v.display('UNK ERROR')
465 Triggered when the user presses a button while entering their pin.
467 def handle_getting_pin_key(self, event, params):
469 if len(self.vstatus.cur_pin) < PIN_LENGTH:
471 if self.vstatus.cur_pin == '':
472 self.vstatus.cur_user = ''
476 self.vstatus.cur_pin = ''
477 self.vstatus.mk.set_message('PIN: ')
479 self.vstatus.cur_pin += chr(key + ord('0'))
480 self.vstatus.mk.set_message('PIN: '+'X'*len(self.vstatus.cur_pin))
481 if len(self.vstatus.cur_pin) == PIN_LENGTH:
482 self.dispense.authUserIdPin(self.vstatus.cur_user, self.vstatus.cur_pin)
483 if self.dispense.getUsername():
484 self.vstatus.username = self.dispense.getUsername()
485 self.v.beep(0, False)
486 self.vstatus.cur_selection = ''
487 self.vstatus.change_state(STATE_GET_SELECTION)
488 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
491 self.v.beep(40, False)
492 self.vstatus.mk.set_messages(
493 [(self.center('BAD PIN'), False, 1.0),
494 (self.center('SORRY'), False, 0.5)])
495 self.vstatus.cur_user = ''
496 self.vstatus.cur_pin = ''
503 Triggered when the user presses a button while entering their user id.
505 def handle_getting_uid_key(self, event, params):
507 # complicated key handling here:
509 if len(self.vstatus.cur_user) == 0 and key == 9:
510 self.vstatus.cur_selection = ''
511 self.vstatus.time_to_autologout = None
512 self.vstatus.mk.set_message('PRICECHECK')
514 self.scroll_options('', self.vstatus.mk)
515 self.vstatus.change_state(STATE_GET_SELECTION)
518 if len(self.vstatus.cur_user) <8:
520 self.vstatus.cur_user = ''
524 self.vstatus.cur_user += chr(key + ord('0'))
525 #logging.info('dob: '+vstatus.cur_user)
526 if len(self.vstatus.cur_user) > 5:
527 self.vstatus.mk.set_message('>'+self.vstatus.cur_user)
529 self.vstatus.mk.set_message('UID: '+self.vstatus.cur_user)
531 if len(self.vstatus.cur_user) == 5:
532 uid = int(self.vstatus.cur_user)
535 logging.info('user '+self.vstatus.cur_user+' has a bad PIN')
541 Welcome to Picklevision Sytems, Sunnyvale, CA
543 Greetings Professor Falken.
548 Shall we play a game?
551 Please choose from the following menu:
558 6. Toxic and Biochemical Warfare
559 7. Global Thermonuclear War
563 Wouldn't you prefer a nice game of chess?
565 """.replace('\n',' ')
566 self.vstatus.mk.set_messages([(pfalken, False, 10)])
567 self.vstatus.cur_user = ''
568 self.vstatus.cur_pin = ''
574 # TODO Fix this up do we can check before logging in
576 if self.dispense.isDisabled():
577 logging.info('user '+self.vstatus.cur_user+' is disabled')
578 self.vstatus.mk.set_messages(
579 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
580 self.vstatus.cur_user = ''
581 self.vstatus.cur_pin = ''
587 self.vstatus.cur_pin = ''
588 self.vstatus.mk.set_message('PIN: ')
589 logging.info('need pin for user %s'%self.vstatus.cur_user)
590 self.vstatus.change_state(STATE_GETTING_PIN)
594 Triggered when a key is pressed and the machine is idling.
596 def handle_idle_key(self, event, params):
599 self.vstatus.cur_user = ''
603 self.vstatus.change_state(STATE_GETTING_UID)
604 self.run_handler(event, params)
607 What to do when there is nothing to do.
609 def handle_idle_tick(self, event, params):
611 if self.vstatus.mk.done():
614 if self.vstatus.time_of_next_idler and time() > self.vstatus.time_of_next_idler:
615 self.vstatus.time_of_next_idler = time() + 30
620 self.vstatus.mk.update_display()
622 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK)
623 self.run_handler(event, params)
627 Manages the beeps for the grandfather clock
629 def beep_on(self, when, before=0):
630 start = int(when - before)
634 if now >= start and now <= end:
638 def handle_idle_grandfather_tick(self, event, params):
639 ### check for interesting times
642 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
643 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
644 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
645 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
647 hourfromnow = localtime(time() + 3600)
649 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
650 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
651 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
653 ## check for X seconds to the hour
654 ## if case, update counter to 2
655 if self.beep_on(onthehour,15) \
656 or self.beep_on(halfhour,0) \
657 or self.beep_on(quarterhour,0) \
658 or self.beep_on(threequarterhour,0) \
659 or self.beep_on(fivetothehour,0):
660 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
661 self.run_handler(event, params)
663 self.vstatus.change_state(STATE_IDLE)
665 def handle_grandfather_tick(self, event, params):
669 ### we live in interesting times
672 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
673 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
674 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
675 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
677 hourfromnow = localtime(time() + 3600)
679 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
680 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
681 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
684 #print "when it fashionable to wear a onion on your hip"
686 if self.beep_on(onthehour,15):
688 next_hour=((hourfromnow[3] + 11) % 12) + 1
689 if onthehour - time() < next_hour and onthehour - time() > 0:
690 self.v.beep(0, False)
694 msg.append(("DING!", False, None))
696 msg.append((" DING!", False, None))
697 elif int(onthehour - time()) == 0:
698 self.v.beep(255, False)
699 msg.append((" BONG!", False, None))
700 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
701 elif self.beep_on(halfhour,0):
703 self.v.beep(0, False)
704 msg.append((" HALFHOUR ", False, 50))
705 elif self.beep_on(quarterhour,0):
707 self.v.beep(0, False)
708 msg.append((" QTR HOUR ", False, 50))
709 elif self.beep_on(threequarterhour,0):
711 self.v.beep(0, False)
712 msg.append((" 3 QTR HR ", False, 50))
713 elif self.beep_on(fivetothehour,0):
715 self.v.beep(0, False)
716 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
720 ## check for X seconds to the hour
723 self.vstatus.mk.set_messages(msg)
726 self.vstatus.mk.update_display()
727 ## if no longer case, return to idle
729 ## change idler to be clock
730 if go_idle and self.vstatus.mk.done():
731 self.vstatus.change_state(STATE_IDLE,1)
734 What to do when the door is open.
736 def handle_door_idle(self, event, params):
737 def twiddle(clock,v,wise = 2):
739 v.display("-FEED ME-")
740 elif (clock % 4 == 1+wise):
741 v.display("\\FEED ME/")
742 elif (clock % 4 == 2):
743 v.display("-FEED ME-")
744 elif (clock % 4 == 3-wise):
745 v.display("/FEED ME\\")
747 # don't care right now.
750 if ((now % 60 % 2) == 0):
753 twiddle(now, self.v, wise=0)
756 What to do when the door is opened or closed.
758 def handle_door_event(self, event, params):
759 if params == 0: #door open
760 self.vstatus.change_state(STATE_DOOR_OPENING)
761 logging.warning("Entering open door mode")
762 self.v.display("-FEED ME-")
764 self.vstatus.cur_user = ''
765 self.vstatus.cur_pin = ''
766 elif params == 1: #door closed
767 self.vstatus.change_state(STATE_DOOR_CLOSING)
770 logging.warning('Leaving open door mode')
771 self.v.display("-YUM YUM!-")
774 Triggered when a user swipes their caed, and the machine is logged out.
776 def handle_mifare_event(self, event, params):
778 # Translate card_id into uid.
779 if card_id == None or card_id == self._last_card_id:
782 self._last_card_id = card_id
784 self.dispense.authMifareCard(card_id)
785 logging.info('Mapped card id to uid %s'%self.dispense.getUsername())
786 if not self.dispense.isLoggedIn():
787 self.v.beep(40, False)
788 self.vstatus.mk.set_messages(
789 [(self.center('BAD CARD'), False, 1.0),
790 (self.center('SORRY'), False, 0.5)])
791 self.vstatus.cur_user = ''
792 self.vstatus.cur_pin = ''
793 self._last_card_id = -1
797 elif self.dispense.isDisabled():
798 self.v.beep(40, False)
799 self.vstatus.mk.set_messages(
800 [(self.center('ACCT DISABLED'), False, 1.0),
801 (self.center('SORRY'), False, 0.5)])
802 self.dispense.logOut()
806 self.vstatus.cur_selection = ''
807 self.vstatus.change_state(STATE_GET_SELECTION)
808 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
812 Triggered when a user swipes their card and the machine is logged in.
814 def handle_mifare_add_user_event(self, event, params):
817 # Translate card_id into uid.
818 if card_id == None or card_id == self._last_card_id:
821 self._last_card_id = card_id
823 if not self.dispense.addCard(card_id):
824 self.vstatus.mk.set_messages(
825 [(self.center('ALREADY'), False, 0.5),
826 (self.center('ENROLLED'), False, 0.5)])
828 self.vstatus.mk.set_messages(
829 [(self.center('CARD'), False, 0.5),
830 (self.center('ENROLLED'), False, 0.5)])
832 def return_to_idle(self, event, params):
836 Maps what to do when the state changes.
838 def create_state_table(self):
839 self.vstatus.state_table[(STATE_IDLE,TICK,1)] = self.handle_idle_tick
840 self.vstatus.state_table[(STATE_IDLE,KEY,1)] = self.handle_idle_key
841 self.vstatus.state_table[(STATE_IDLE,DOOR,1)] = self.handle_door_event
842 self.vstatus.state_table[(STATE_IDLE,MIFARE,1)] = self.handle_mifare_event
844 self.vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = self.handle_door_idle
845 self.vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = self.handle_door_event
846 self.vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = self.do_nothing
847 self.vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = self.do_nothing
849 self.vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = self.return_to_idle
850 self.vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = self.handle_door_event
851 self.vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = self.do_nothing
852 self.vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = self.do_nothing
854 self.vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = self.handle_getting_uid_idle
855 self.vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = self.handle_door_event
856 self.vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = self.handle_getting_uid_key
857 self.vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = self.handle_mifare_event
859 self.vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = self.handle_getting_pin_idle
860 self.vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = self.handle_door_event
861 self.vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = self.handle_getting_pin_key
862 self.vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = self.handle_mifare_event
864 self.vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = self.handle_get_selection_idle
865 self.vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = self.handle_door_event
866 self.vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = self.handle_get_selection_key
867 self.vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = self.handle_mifare_add_user_event
869 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = self.handle_idle_grandfather_tick
870 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = self.handle_grandfather_tick
871 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = self.handle_door_event
872 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = self.handle_door_event
873 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = self.do_nothing
874 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = self.do_nothing
875 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = self.handle_mifare_event
878 Get what to do on a state change.
880 def get_state_table_handler(self, state, event, counter):
881 return self.vstatus.state_table[(state,event,counter)]
883 def time_to_next_update(self):
884 idle_update = self.vstatus.time_of_next_idlestep - time()
885 if not self.vstatus.mk.done() and self.vstatus.mk.next_update is not None:
886 mk_update = self.vstatus.mk.next_update - time()
887 if mk_update < idle_update:
888 idle_update = mk_update
891 def run_forever(self, rfh, wfh, options, cf):
892 self.v = VendingMachine(rfh, wfh, USE_MIFARE)
893 self.dispense = Dispense()
894 self.vstatus = VendState(self.v)
895 self.create_state_table()
897 logging.debug('PING is ' + str(self.v.ping()))
903 timeout = self.time_to_next_update()
904 (event, params) = self.v.next_event(timeout)
905 self.run_handler(event, params)
907 def run_handler(self, event, params):
908 handler = self.get_state_table_handler(self.vstatus.state,event,self.vstatus.counter)
910 handler(event, params)
913 Connect to the machine.
915 def connect_to_vend(options, cf):
918 logging.info('Connecting to vending machine using LAT')
919 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
920 rfh, wfh = latclient.get_fh()
921 elif options.use_serial:
922 # Open vending machine via serial.
923 logging.info('Connecting to vending machine using serial')
924 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
925 rfh,wfh = serialclient.get_fh()
927 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
928 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
930 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
931 sock.connect((options.host, options.port))
932 rfh = sock.makefile('r')
933 wfh = sock.makefile('w')
940 Parse arguments from the command line
943 from optparse import OptionParser
945 op = OptionParser(usage="%prog [OPTION]...")
946 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')
947 op.add_option('--serial', action='store_true', default=False, dest='use_serial', help='use the serial port')
948 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
949 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
950 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
951 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
952 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
953 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
954 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
955 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
956 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
957 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
958 options, args = op.parse_args()
961 op.error('extra command line arguments: ' + ' '.join(args))
965 def create_pid_file(name):
967 pid_file = file(name, 'w')
968 pid_file.write('%d\n'%os.getpid())
971 logging.warning('unable to write to pid file '+name+': '+str(e))
974 def do_nothing(signum, stack):
975 signal.signal(signum, do_nothing)
976 def stop_server(signum, stack): raise KeyboardInterrupt
977 signal.signal(signal.SIGHUP, do_nothing)
978 signal.signal(signal.SIGTERM, stop_server)
979 signal.signal(signal.SIGINT, stop_server)
981 options = parse_args()
982 config_opts = VendConfigFile(options.config_file, config_options)
983 if options.daemon: become_daemon()
984 set_up_logging(options)
985 if options.pid_file != '': create_pid_file(options.pid_file)
987 return options, config_opts
989 def clean_up_nicely(options, config_opts):
990 if options.pid_file != '':
992 os.unlink(options.pid_file)
993 logging.debug('Removed pid file '+options.pid_file)
994 except OSError: pass # if we can't delete it, meh
996 def set_up_logging(options):
997 logger = logging.getLogger()
999 if not options.daemon:
1000 stderr_logger = logging.StreamHandler(sys.stderr)
1001 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1002 logger.addHandler(stderr_logger)
1004 if options.log_file != '':
1006 file_logger = logging.FileHandler(options.log_file)
1007 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1008 logger.addHandler(file_logger)
1010 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1012 if options.syslog != None:
1013 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1014 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1015 logger.addHandler(sys_logger)
1018 logger.setLevel(logging.WARNING)
1019 elif options.verbose:
1020 logger.setLevel(logging.DEBUG)
1022 logger.setLevel(logging.INFO)
1024 def become_daemon():
1025 dev_null = file('/dev/null')
1026 fd = dev_null.fileno()
1035 raise SystemExit('failed to fork: '+str(e))
1037 def do_vend_server(options, config_opts):
1040 rfh, wfh = connect_to_vend(options, config_opts)
1041 except (SerialClientException, socket.error), e:
1042 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1044 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1045 logging.info("Trying again in 5 seconds.")
1049 # run_forever(rfh, wfh, options, config_opts)
1052 vserver = VendServer()
1053 vserver.run_forever(rfh, wfh, options, config_opts)
1054 except VendingException:
1055 logging.error("Connection died, trying again...")
1056 logging.info("Trying again in 5 seconds.")
1060 def main(argv=None):
1061 options, config_opts = set_stuff_up()
1064 logging.warning('Starting Vend Server')
1065 do_vend_server(options, config_opts)
1066 logging.error('Vend Server finished unexpectedly, restarting')
1067 except KeyboardInterrupt:
1068 logging.info("Killed by signal, cleaning up")
1069 clean_up_nicely(options, config_opts)
1070 logging.warning("Vend Server stopped")
1075 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1076 tb = format_tb(exc_traceback, 20)
1079 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1080 logging.critical("Message: " + str(exc_value))
1081 logging.critical("Traceback:")
1083 for line in event.split('\n'):
1084 logging.critical(' '+line)
1085 logging.critical("This message should be considered a bug in the Vend Server.")
1086 logging.critical("Please report this to someone who can fix it.")
1088 logging.warning("Trying again anyway (might not help, but hey...)")
1090 if __name__ == '__main__':