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.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 logging.debug('Dispense item "%s"' % (self.vstatus.cur_selection,))
409 # should use sudo here
410 if self.vstatus.cur_selection == '55':
411 self.vstatus.mk.set_message('OPENSESAME')
412 logging.info('dispensing a door for %s'%self.vstatus.username)
414 ret = os.system('dispense -u "%s" door'%self.vstatus.username)
416 ret = os.system('dispense door')
418 logging.info('door opened')
419 self.vstatus.mk.set_message(self.center('DOOR OPEN'))
421 logging.warning('user %s tried to dispense a bad door'%self.vstatus.username)
422 self.vstatus.mk.set_message(self.center('BAD DOOR'))
424 elif self.vstatus.cur_selection == '81':
426 elif self.vstatus.cur_selection == '99':
427 self.scroll_options(self.vstatus.username, self.vstatus.mk)
428 self.vstatus.cur_selection = ''
430 elif self.vstatus.cur_selection[1] == '8':
431 self.v.display('GOT DRINK?')
432 if ((os.system('dispense -u "%s" coke:%s'%(self.vstatus.username, self.vstatus.cur_selection[0])) >> 8) != 0):
433 self.v.display('SEEMS NOT')
435 self.v.display('GOT DRINK!')
437 # first see if it's a named slot
439 price, shortname, name = get_snack( self.vstatus.cur_selection )
441 price, shortname, name = get_snack( '--' )
442 dollarprice = "$%.2f" % ( price / 100.0 )
443 self.v.display(self.vstatus.cur_selection+' - %s'%dollarprice)
444 exitcode = os.system('dispense -u "%s" snack:%s'%(self.vstatus.username, self.vstatus.cur_selection)) >> 8
446 # magic dispense syslog service
447 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, self.vstatus.cur_selection, self.vstatus.username))
448 (worked, code, string) = self.v.vend(self.vstatus.cur_selection)
450 self.v.display('THANK YOU')
452 print "Vend Failed:", code, string
453 self.v.display('VEND FAIL')
454 elif (exitcode == 5): # RV_BALANCE
455 self.v.display('NO MONEY?')
456 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
457 self.v.display('EMPTY SLOT')
458 elif (exitcode == 1): # RV_BADITEM (Dead slot)
459 self.v.display('EMPTY SLOT')
461 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))
462 self.v.display('UNK ERROR')
466 Triggered when the user presses a button while entering their pin.
468 def handle_getting_pin_key(self, event, params):
470 if len(self.vstatus.cur_pin) < PIN_LENGTH:
472 if self.vstatus.cur_pin == '':
473 self.vstatus.cur_user = ''
477 self.vstatus.cur_pin = ''
478 self.vstatus.mk.set_message('PIN: ')
480 self.vstatus.cur_pin += chr(key + ord('0'))
481 self.vstatus.mk.set_message('PIN: '+'X'*len(self.vstatus.cur_pin))
482 if len(self.vstatus.cur_pin) == PIN_LENGTH:
483 self.dispense.authUserIdPin(self.vstatus.cur_user, self.vstatus.cur_pin)
484 if self.dispense.getUsername():
485 self.vstatus.username = self.dispense.getUsername()
486 self.v.beep(0, False)
487 self.vstatus.cur_selection = ''
488 self.vstatus.change_state(STATE_GET_SELECTION)
489 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
492 self.v.beep(40, False)
493 self.vstatus.mk.set_messages(
494 [(self.center('BAD PIN'), False, 1.0),
495 (self.center('SORRY'), False, 0.5)])
496 self.vstatus.cur_user = ''
497 self.vstatus.cur_pin = ''
504 Triggered when the user presses a button while entering their user id.
506 def handle_getting_uid_key(self, event, params):
508 # complicated key handling here:
510 if len(self.vstatus.cur_user) == 0 and key == 9:
511 self.vstatus.cur_selection = ''
512 self.vstatus.time_to_autologout = None
513 self.vstatus.mk.set_message('PRICECHECK')
515 self.scroll_options('', self.vstatus.mk)
516 self.vstatus.change_state(STATE_GET_SELECTION)
519 if len(self.vstatus.cur_user) <8:
521 self.vstatus.cur_user = ''
525 self.vstatus.cur_user += chr(key + ord('0'))
526 #logging.info('dob: '+vstatus.cur_user)
527 if len(self.vstatus.cur_user) > 5:
528 self.vstatus.mk.set_message('>'+self.vstatus.cur_user)
530 self.vstatus.mk.set_message('UID: '+self.vstatus.cur_user)
532 if len(self.vstatus.cur_user) == 5:
533 uid = int(self.vstatus.cur_user)
536 logging.info('user '+self.vstatus.cur_user+' has a bad PIN')
542 Welcome to Picklevision Sytems, Sunnyvale, CA
544 Greetings Professor Falken.
549 Shall we play a game?
552 Please choose from the following menu:
559 6. Toxic and Biochemical Warfare
560 7. Global Thermonuclear War
564 Wouldn't you prefer a nice game of chess?
566 """.replace('\n',' ')
567 self.vstatus.mk.set_messages([(pfalken, False, 10)])
568 self.vstatus.cur_user = ''
569 self.vstatus.cur_pin = ''
575 # TODO Fix this up do we can check before logging in
577 if self.dispense.isDisabled():
578 logging.info('user '+self.vstatus.cur_user+' is disabled')
579 self.vstatus.mk.set_messages(
580 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
581 self.vstatus.cur_user = ''
582 self.vstatus.cur_pin = ''
588 self.vstatus.cur_pin = ''
589 self.vstatus.mk.set_message('PIN: ')
590 logging.info('need pin for user %s'%self.vstatus.cur_user)
591 self.vstatus.change_state(STATE_GETTING_PIN)
595 Triggered when a key is pressed and the machine is idling.
597 def handle_idle_key(self, event, params):
600 self.vstatus.cur_user = ''
604 self.vstatus.change_state(STATE_GETTING_UID)
605 self.run_handler(event, params)
608 What to do when there is nothing to do.
610 def handle_idle_tick(self, event, params):
612 if self.vstatus.mk.done():
615 if self.vstatus.time_of_next_idler and time() > self.vstatus.time_of_next_idler:
616 self.vstatus.time_of_next_idler = time() + 30
621 self.vstatus.mk.update_display()
623 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK)
624 self.run_handler(event, params)
628 Manages the beeps for the grandfather clock
630 def beep_on(self, when, before=0):
631 start = int(when - before)
635 if now >= start and now <= end:
639 def handle_idle_grandfather_tick(self, event, params):
640 ### check for interesting times
643 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
644 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
645 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
646 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
648 hourfromnow = localtime(time() + 3600)
650 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
651 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
652 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
654 ## check for X seconds to the hour
655 ## if case, update counter to 2
656 if self.beep_on(onthehour,15) \
657 or self.beep_on(halfhour,0) \
658 or self.beep_on(quarterhour,0) \
659 or self.beep_on(threequarterhour,0) \
660 or self.beep_on(fivetothehour,0):
661 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
662 self.run_handler(event, params)
664 self.vstatus.change_state(STATE_IDLE)
666 def handle_grandfather_tick(self, event, params):
670 ### we live in interesting times
673 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
674 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
675 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
676 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
678 hourfromnow = localtime(time() + 3600)
680 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
681 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
682 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
685 #print "when it fashionable to wear a onion on your hip"
687 if self.beep_on(onthehour,15):
689 next_hour=((hourfromnow[3] + 11) % 12) + 1
690 if onthehour - time() < next_hour and onthehour - time() > 0:
691 self.v.beep(0, False)
695 msg.append(("DING!", False, None))
697 msg.append((" DING!", False, None))
698 elif int(onthehour - time()) == 0:
699 self.v.beep(255, False)
700 msg.append((" BONG!", False, None))
701 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
702 elif self.beep_on(halfhour,0):
704 self.v.beep(0, False)
705 msg.append((" HALFHOUR ", False, 50))
706 elif self.beep_on(quarterhour,0):
708 self.v.beep(0, False)
709 msg.append((" QTR HOUR ", False, 50))
710 elif self.beep_on(threequarterhour,0):
712 self.v.beep(0, False)
713 msg.append((" 3 QTR HR ", False, 50))
714 elif self.beep_on(fivetothehour,0):
716 self.v.beep(0, False)
717 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
721 ## check for X seconds to the hour
724 self.vstatus.mk.set_messages(msg)
727 self.vstatus.mk.update_display()
728 ## if no longer case, return to idle
730 ## change idler to be clock
731 if go_idle and self.vstatus.mk.done():
732 self.vstatus.change_state(STATE_IDLE,1)
735 What to do when the door is open.
737 def handle_door_idle(self, event, params):
738 def twiddle(clock,v,wise = 2):
740 v.display("-FEED ME-")
741 elif (clock % 4 == 1+wise):
742 v.display("\\FEED ME/")
743 elif (clock % 4 == 2):
744 v.display("-FEED ME-")
745 elif (clock % 4 == 3-wise):
746 v.display("/FEED ME\\")
748 # don't care right now.
751 if ((now % 60 % 2) == 0):
754 twiddle(now, self.v, wise=0)
757 What to do when the door is opened or closed.
759 def handle_door_event(self, event, params):
760 if params == 0: #door open
761 self.vstatus.change_state(STATE_DOOR_OPENING)
762 logging.warning("Entering open door mode")
763 self.v.display("-FEED ME-")
765 self.vstatus.cur_user = ''
766 self.vstatus.cur_pin = ''
767 elif params == 1: #door closed
768 self.vstatus.change_state(STATE_DOOR_CLOSING)
771 logging.warning('Leaving open door mode')
772 self.v.display("-YUM YUM!-")
775 Triggered when a user swipes their caed, and the machine is logged out.
777 def handle_mifare_event(self, event, params):
779 # Translate card_id into uid.
780 if card_id == None or card_id == self._last_card_id:
783 self._last_card_id = card_id
785 self.dispense.authMifareCard(card_id)
786 logging.info('Mapped card id to uid %s'%self.dispense.getUsername())
787 if not self.dispense.isLoggedIn():
788 self.v.beep(40, False)
789 self.vstatus.mk.set_messages(
790 [(self.center('BAD CARD'), False, 1.0),
791 (self.center('SORRY'), False, 0.5)])
792 self.vstatus.cur_user = ''
793 self.vstatus.cur_pin = ''
794 self._last_card_id = -1
798 elif self.dispense.isDisabled():
799 self.v.beep(40, False)
800 self.vstatus.mk.set_messages(
801 [(self.center('ACCT DISABLED'), False, 1.0),
802 (self.center('SORRY'), False, 0.5)])
803 self.dispense.logOut()
807 self.vstatus.cur_user = '----'
808 self.vstatus.username = self.dispense.getUsername()
809 self.vstatus.cur_selection = ''
810 self.vstatus.change_state(STATE_GET_SELECTION)
811 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
815 Triggered when a user swipes their card and the machine is logged in.
817 def handle_mifare_add_user_event(self, event, params):
820 # Translate card_id into uid.
821 if card_id == None or card_id == self._last_card_id:
824 self._last_card_id = card_id
826 if not self.dispense.addCard(card_id):
827 self.vstatus.mk.set_messages(
828 [(self.center('ALREADY'), False, 0.5),
829 (self.center('ENROLLED'), False, 0.5)])
831 self.vstatus.mk.set_messages(
832 [(self.center('CARD'), False, 0.5),
833 (self.center('ENROLLED'), False, 0.5)])
835 def return_to_idle(self, event, params):
839 Maps what to do when the state changes.
841 def create_state_table(self):
842 self.vstatus.state_table[(STATE_IDLE,TICK,1)] = self.handle_idle_tick
843 self.vstatus.state_table[(STATE_IDLE,KEY,1)] = self.handle_idle_key
844 self.vstatus.state_table[(STATE_IDLE,DOOR,1)] = self.handle_door_event
845 self.vstatus.state_table[(STATE_IDLE,MIFARE,1)] = self.handle_mifare_event
847 self.vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = self.handle_door_idle
848 self.vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = self.handle_door_event
849 self.vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = self.do_nothing
850 self.vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = self.do_nothing
852 self.vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = self.return_to_idle
853 self.vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = self.handle_door_event
854 self.vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = self.do_nothing
855 self.vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = self.do_nothing
857 self.vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = self.handle_getting_uid_idle
858 self.vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = self.handle_door_event
859 self.vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = self.handle_getting_uid_key
860 self.vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = self.handle_mifare_event
862 self.vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = self.handle_getting_pin_idle
863 self.vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = self.handle_door_event
864 self.vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = self.handle_getting_pin_key
865 self.vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = self.handle_mifare_event
867 self.vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = self.handle_get_selection_idle
868 self.vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = self.handle_door_event
869 self.vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = self.handle_get_selection_key
870 self.vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = self.handle_mifare_add_user_event
872 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = self.handle_idle_grandfather_tick
873 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = self.handle_grandfather_tick
874 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = self.handle_door_event
875 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = self.handle_door_event
876 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = self.do_nothing
877 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = self.do_nothing
878 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = self.handle_mifare_event
881 Get what to do on a state change.
883 def get_state_table_handler(self, state, event, counter):
884 return self.vstatus.state_table[(state,event,counter)]
886 def time_to_next_update(self):
887 idle_update = self.vstatus.time_of_next_idlestep - time()
888 if not self.vstatus.mk.done() and self.vstatus.mk.next_update is not None:
889 mk_update = self.vstatus.mk.next_update - time()
890 if mk_update < idle_update:
891 idle_update = mk_update
894 def run_forever(self, rfh, wfh, options, cf):
895 self.v = VendingMachine(rfh, wfh, USE_MIFARE)
896 self.dispense = Dispense()
897 self.vstatus = VendState(self.v)
898 self.create_state_table()
900 logging.debug('PING is ' + str(self.v.ping()))
906 timeout = self.time_to_next_update()
907 (event, params) = self.v.next_event(timeout)
908 self.run_handler(event, params)
910 def run_handler(self, event, params):
911 handler = self.get_state_table_handler(self.vstatus.state,event,self.vstatus.counter)
913 handler(event, params)
916 Connect to the machine.
918 def connect_to_vend(options, cf):
921 logging.info('Connecting to vending machine using LAT')
922 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
923 rfh, wfh = latclient.get_fh()
924 elif options.use_serial:
925 # Open vending machine via serial.
926 logging.info('Connecting to vending machine using serial')
927 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
928 rfh,wfh = serialclient.get_fh()
930 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
931 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
933 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
934 sock.connect((options.host, options.port))
935 rfh = sock.makefile('r')
936 wfh = sock.makefile('w')
943 Parse arguments from the command line
946 from optparse import OptionParser
948 op = OptionParser(usage="%prog [OPTION]...")
949 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')
950 op.add_option('--serial', action='store_true', default=False, dest='use_serial', help='use the serial port')
951 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
952 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
953 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
954 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
955 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
956 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
957 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
958 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
959 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
960 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
961 options, args = op.parse_args()
964 op.error('extra command line arguments: ' + ' '.join(args))
968 def create_pid_file(name):
970 pid_file = file(name, 'w')
971 pid_file.write('%d\n'%os.getpid())
974 logging.warning('unable to write to pid file '+name+': '+str(e))
977 def do_nothing(signum, stack):
978 signal.signal(signum, do_nothing)
979 def stop_server(signum, stack): raise KeyboardInterrupt
980 signal.signal(signal.SIGHUP, do_nothing)
981 signal.signal(signal.SIGTERM, stop_server)
982 signal.signal(signal.SIGINT, stop_server)
984 options = parse_args()
985 config_opts = VendConfigFile(options.config_file, config_options)
986 if options.daemon: become_daemon()
987 set_up_logging(options)
988 if options.pid_file != '': create_pid_file(options.pid_file)
990 return options, config_opts
992 def clean_up_nicely(options, config_opts):
993 if options.pid_file != '':
995 os.unlink(options.pid_file)
996 logging.debug('Removed pid file '+options.pid_file)
997 except OSError: pass # if we can't delete it, meh
999 def set_up_logging(options):
1000 logger = logging.getLogger()
1002 if not options.daemon:
1003 stderr_logger = logging.StreamHandler(sys.stderr)
1004 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1005 logger.addHandler(stderr_logger)
1007 if options.log_file != '':
1009 file_logger = logging.FileHandler(options.log_file)
1010 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1011 logger.addHandler(file_logger)
1013 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1015 if options.syslog != None:
1016 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1017 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1018 logger.addHandler(sys_logger)
1021 logger.setLevel(logging.WARNING)
1022 elif options.verbose:
1023 logger.setLevel(logging.DEBUG)
1025 logger.setLevel(logging.INFO)
1027 def become_daemon():
1028 dev_null = file('/dev/null')
1029 fd = dev_null.fileno()
1038 raise SystemExit('failed to fork: '+str(e))
1040 def do_vend_server(options, config_opts):
1043 rfh, wfh = connect_to_vend(options, config_opts)
1044 except (SerialClientException, socket.error), e:
1045 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1047 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1048 logging.info("Trying again in 5 seconds.")
1052 # run_forever(rfh, wfh, options, config_opts)
1055 vserver = VendServer()
1056 vserver.run_forever(rfh, wfh, options, config_opts)
1057 except VendingException:
1058 logging.error("Connection died, trying again...")
1059 logging.info("Trying again in 5 seconds.")
1063 def main(argv=None):
1064 options, config_opts = set_stuff_up()
1067 logging.warning('Starting Vend Server')
1068 do_vend_server(options, config_opts)
1069 logging.error('Vend Server finished unexpectedly, restarting')
1070 except KeyboardInterrupt:
1071 logging.info("Killed by signal, cleaning up")
1072 clean_up_nicely(options, config_opts)
1073 logging.warning("Vend Server stopped")
1078 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1079 tb = format_tb(exc_traceback, 20)
1082 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1083 logging.critical("Message: " + str(exc_value))
1084 logging.critical("Traceback:")
1086 for line in event.split('\n'):
1087 logging.critical(' '+line)
1088 logging.critical("This message should be considered a bug in the Vend Server.")
1089 logging.critical("Please report this to someone who can fix it.")
1091 logging.warning("Trying again anyway (might not help, but hey...)")
1093 if __name__ == '__main__':