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 LDAPConnector import get_uid,get_uname, set_card_id
23 from OpenDispense import OpenDispense as Dispense
26 This vending machine software brought to you by:
31 and a collective of hungry alpacas.
33 The MIFARE card reader bought to you by:
36 Bug Hunting and hardware maintenance by:
39 For a good time call +61 8 6488 3901
61 STATE_GRANDFATHER_CLOCK,
69 'DBServer': ('Database', 'Server'),
70 'DBName': ('Database', 'Name'),
71 'DBUser': ('VendingMachine', 'DBUser'),
72 'DBPassword': ('VendingMachine', 'DBPassword'),
74 'ServiceName': ('VendingMachine', 'ServiceName'),
75 'ServicePassword': ('VendingMachine', 'Password'),
77 'ServerName': ('DecServer', 'Name'),
78 'ConnectPassword': ('DecServer', 'ConnectPassword'),
79 'PrivPassword': ('DecServer', 'PrivPassword'),
83 def __init__(self, config_file, options):
85 cp = ConfigParser.ConfigParser()
88 for option in options:
89 section, name = options[option]
90 value = cp.get(section, name)
91 self.__dict__[option] = value
93 except ConfigParser.Error, e:
94 raise SystemExit("Error reading config file "+config_file+": " + str(e))
97 This class manages the current state of the vending machine.
100 def __init__(self,v):
101 self.state_table = {}
102 self.state = STATE_IDLE
105 self.mk = MessageKeeper(v)
109 self.cur_selection = ''
110 self.time_to_autologout = None
112 self.last_timeout_refresh = None
114 def change_state(self,newstate,newcounter=None):
115 if self.state != newstate:
116 self.state = newstate
118 if newcounter is not None and self.counter != newcounter:
119 self.counter = newcounter
138 Show information to the user as to what can be dispensed.
140 def scroll_options(self, username, mk, welcome = False):
141 # If the user has just logged in, show them their balance
143 balance = self.dispense.getBalance()
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 self.dispense.getItemInfo(self.vstatus.cur_selection)
396 self.vstatus.cur_selection = ''
397 self.vstatus.time_to_autologout = None
398 self.vstatus.last_timeout_refresh = None
401 Triggered when the user has entered the id of something they would like to purchase.
403 def make_selection(self):
404 # should use sudo here
405 if self.vstatus.cur_selection == '55':
406 self.vstatus.mk.set_message('OPENSESAME')
407 logging.info('dispensing a door for %s'%self.vstatus.username)
409 ret = os.system('dispense -u "%s" door'%self.vstatus.username)
411 ret = os.system('dispense door')
413 logging.info('door opened')
414 self.vstatus.mk.set_message(self.center('DOOR OPEN'))
416 logging.warning('user %s tried to dispense a bad door'%self.vstatus.username)
417 self.vstatus.mk.set_message(self.center('BAD DOOR'))
419 elif self.vstatus.cur_selection == '81':
421 elif self.vstatus.cur_selection == '99':
422 self.scroll_options(self.vstatus.username, self.vstatus.mk)
423 self.vstatus.cur_selection = ''
425 elif self.vstatus.cur_selection[1] == '8':
426 self.v.display('GOT DRINK?')
427 if ((os.system('dispense -u "%s" coke:%s'%(self.vstatus.username, self.vstatus.cur_selection[0])) >> 8) != 0):
428 self.v.display('SEEMS NOT')
430 self.v.display('GOT DRINK!')
432 # first see if it's a named slot
434 price, shortname, name = get_snack( self.vstatus.cur_selection )
436 price, shortname, name = get_snack( '--' )
437 dollarprice = "$%.2f" % ( price / 100.0 )
438 self.v.display(self.vstatus.cur_selection+' - %s'%dollarprice)
439 exitcode = os.system('dispense -u "%s" snack:%s'%(self.vstatus.username, self.vstatus.cur_selection)) >> 8
441 # magic dispense syslog service
442 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, self.vstatus.cur_selection, self.vstatus.username))
443 (worked, code, string) = self.v.vend(self.vstatus.cur_selection)
445 self.v.display('THANK YOU')
447 print "Vend Failed:", code, string
448 self.v.display('VEND FAIL')
449 elif (exitcode == 5): # RV_BALANCE
450 self.v.display('NO MONEY?')
451 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
452 self.v.display('EMPTY SLOT')
453 elif (exitcode == 1): # RV_BADITEM (Dead slot)
454 self.v.display('EMPTY SLOT')
456 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))
457 self.v.display('UNK ERROR')
461 Triggered when the user presses a button while entering their pin.
463 def handle_getting_pin_key(self, event, params):
465 if len(self.vstatus.cur_pin) < PIN_LENGTH:
467 if self.vstatus.cur_pin == '':
468 self.vstatus.cur_user = ''
472 self.vstatus.cur_pin = ''
473 self.vstatus.mk.set_message('PIN: ')
475 self.vstatus.cur_pin += chr(key + ord('0'))
476 self.vstatus.mk.set_message('PIN: '+'X'*len(self.vstatus.cur_pin))
477 if len(self.vstatus.cur_pin) == PIN_LENGTH:
478 self.dispense.authUserIdPin(self.vstatus.cur_user, self.vstatus.cur_pin)
479 if self.dispense.getUsername():
480 self.v.beep(0, False)
481 self.vstatus.cur_selection = ''
482 self.vstatus.change_state(STATE_GET_SELECTION)
483 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
486 self.v.beep(40, False)
487 self.vstatus.mk.set_messages(
488 [(self.center('BAD PIN'), False, 1.0),
489 (self.center('SORRY'), False, 0.5)])
490 self.vstatus.cur_user = ''
491 self.vstatus.cur_pin = ''
498 Triggered when the user presses a button while entering their user id.
500 def handle_getting_uid_key(self, event, params):
502 # complicated key handling here:
504 if len(self.vstatus.cur_user) == 0 and key == 9:
505 self.vstatus.cur_selection = ''
506 self.vstatus.time_to_autologout = None
507 self.vstatus.mk.set_message('PRICECHECK')
509 self.scroll_options('', self.vstatus.mk)
510 self.vstatus.change_state(STATE_GET_SELECTION)
513 if len(self.vstatus.cur_user) <8:
515 self.vstatus.cur_user = ''
519 self.vstatus.cur_user += chr(key + ord('0'))
520 #logging.info('dob: '+vstatus.cur_user)
521 if len(self.vstatus.cur_user) > 5:
522 self.vstatus.mk.set_message('>'+self.vstatus.cur_user)
524 self.vstatus.mk.set_message('UID: '+self.vstatus.cur_user)
526 if len(self.vstatus.cur_user) == 5:
527 uid = int(self.vstatus.cur_user)
530 logging.info('user '+self.vstatus.cur_user+' has a bad PIN')
536 Welcome to Picklevision Sytems, Sunnyvale, CA
538 Greetings Professor Falken.
543 Shall we play a game?
546 Please choose from the following menu:
553 6. Toxic and Biochemical Warfare
554 7. Global Thermonuclear War
558 Wouldn't you prefer a nice game of chess?
560 """.replace('\n',' ')
561 self.vstatus.mk.set_messages([(pfalken, False, 10)])
562 self.vstatus.cur_user = ''
563 self.vstatus.cur_pin = ''
569 # TODO Fix this up do we can check before logging in
571 if self.dispense.isDisabled():
572 logging.info('user '+self.vstatus.cur_user+' is disabled')
573 self.vstatus.mk.set_messages(
574 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
575 self.vstatus.cur_user = ''
576 self.vstatus.cur_pin = ''
582 self.vstatus.cur_pin = ''
583 self.vstatus.mk.set_message('PIN: ')
584 logging.info('need pin for user %s'%self.vstatus.cur_user)
585 self.vstatus.change_state(STATE_GETTING_PIN)
589 Triggered when a key is pressed and the machine is idling.
591 def handle_idle_key(self, event, params):
594 self.vstatus.cur_user = ''
598 self.vstatus.change_state(STATE_GETTING_UID)
599 self.run_handler(event, params)
602 What to do when there is nothing to do.
604 def handle_idle_tick(self, event, params):
606 if self.vstatus.mk.done():
609 if self.vstatus.time_of_next_idler and time() > self.vstatus.time_of_next_idler:
610 self.vstatus.time_of_next_idler = time() + 30
615 self.vstatus.mk.update_display()
617 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK)
618 self.run_handler(event, params)
622 Manages the beeps for the grandfather clock
624 def beep_on(self, when, before=0):
625 start = int(when - before)
629 if now >= start and now <= end:
633 def handle_idle_grandfather_tick(self, event, params):
634 ### check for interesting times
637 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
638 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
639 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
640 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
642 hourfromnow = localtime(time() + 3600)
644 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
645 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
646 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
648 ## check for X seconds to the hour
649 ## if case, update counter to 2
650 if self.beep_on(onthehour,15) \
651 or self.beep_on(halfhour,0) \
652 or self.beep_on(quarterhour,0) \
653 or self.beep_on(threequarterhour,0) \
654 or self.beep_on(fivetothehour,0):
655 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
656 self.run_handler(event, params)
658 self.vstatus.change_state(STATE_IDLE)
660 def handle_grandfather_tick(self, event, params):
664 ### we live in interesting times
667 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
668 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
669 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
670 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
672 hourfromnow = localtime(time() + 3600)
674 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
675 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
676 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
679 #print "when it fashionable to wear a onion on your hip"
681 if self.beep_on(onthehour,15):
683 next_hour=((hourfromnow[3] + 11) % 12) + 1
684 if onthehour - time() < next_hour and onthehour - time() > 0:
685 self.v.beep(0, False)
689 msg.append(("DING!", False, None))
691 msg.append((" DING!", False, None))
692 elif int(onthehour - time()) == 0:
693 self.v.beep(255, False)
694 msg.append((" BONG!", False, None))
695 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
696 elif self.beep_on(halfhour,0):
698 self.v.beep(0, False)
699 msg.append((" HALFHOUR ", False, 50))
700 elif self.beep_on(quarterhour,0):
702 self.v.beep(0, False)
703 msg.append((" QTR HOUR ", False, 50))
704 elif self.beep_on(threequarterhour,0):
706 self.v.beep(0, False)
707 msg.append((" 3 QTR HR ", False, 50))
708 elif self.beep_on(fivetothehour,0):
710 self.v.beep(0, False)
711 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
715 ## check for X seconds to the hour
718 self.vstatus.mk.set_messages(msg)
721 self.vstatus.mk.update_display()
722 ## if no longer case, return to idle
724 ## change idler to be clock
725 if go_idle and self.vstatus.mk.done():
726 self.vstatus.change_state(STATE_IDLE,1)
729 What to do when the door is open.
731 def handle_door_idle(self, event, params):
732 def twiddle(clock,v,wise = 2):
734 v.display("-FEED ME-")
735 elif (clock % 4 == 1+wise):
736 v.display("\\FEED ME/")
737 elif (clock % 4 == 2):
738 v.display("-FEED ME-")
739 elif (clock % 4 == 3-wise):
740 v.display("/FEED ME\\")
742 # don't care right now.
745 if ((now % 60 % 2) == 0):
748 twiddle(now, self.v, wise=0)
751 What to do when the door is opened or closed.
753 def handle_door_event(self, event, params):
754 if params == 0: #door open
755 self.vstatus.change_state(STATE_DOOR_OPENING)
756 logging.warning("Entering open door mode")
757 self.v.display("-FEED ME-")
759 self.vstatus.cur_user = ''
760 self.vstatus.cur_pin = ''
761 elif params == 1: #door closed
762 self.vstatus.change_state(STATE_DOOR_CLOSING)
765 logging.warning('Leaving open door mode')
766 self.v.display("-YUM YUM!-")
769 Triggered when a user swipes their caed, and the machine is logged out.
771 def handle_mifare_event(self, event, params):
773 # Translate card_id into uid.
774 if card_id == None or card_id == self._last_card_id:
777 self._last_card_id = card_id
779 self.dispense.authMifareCard(card_id)
780 logging.info('Mapped card id to uid %s'%self.dispense.getUsername())
781 if not self.dispense.isLoggedIn():
782 self.v.beep(40, False)
783 self.vstatus.mk.set_messages(
784 [(self.center('BAD CARD'), False, 1.0),
785 (self.center('SORRY'), False, 0.5)])
786 self.vstatus.cur_user = ''
787 self.vstatus.cur_pin = ''
788 self._last_card_id = -1
792 elif self.dispense.isDisabled():
793 self.v.beep(40, False)
794 self.vstatus.mk.set_messages(
795 [(self.center('ACCT DISABLED'), False, 1.0),
796 (self.center('SORRY'), False, 0.5)])
797 self.dispense.logOut()
801 self.vstatus.cur_selection = ''
802 self.vstatus.change_state(STATE_GET_SELECTION)
803 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
807 Triggered when a user swipes their card and the machine is logged in.
809 def handle_mifare_add_user_event(self, event, params):
812 # Translate card_id into uid.
813 if card_id == None or card_id == self._last_card_id:
816 self._last_card_id = card_id
818 res = self.dispense.addCard(card_id)
820 if get_uid(card_id) != None:
821 self.vstatus.mk.set_messages(
822 [(self.center('ALREADY'), False, 0.5),
823 (self.center('ENROLLED'), False, 0.5)])
825 logging.info('Enrolling card %s to uid %s (%s)'%(card_id, self.vstatus.cur_user, self.vstatus.username))
826 self.set_card_id(self.vstatus.cur_user, self.card_id)
827 self.vstatus.mk.set_messages(
828 [(self.center('CARD'), False, 0.5),
829 (self.center('ENROLLED'), False, 0.5)])
831 def return_to_idle(self, event, params):
835 Maps what to do when the state changes.
837 def create_state_table(self):
838 self.vstatus.state_table[(STATE_IDLE,TICK,1)] = self.handle_idle_tick
839 self.vstatus.state_table[(STATE_IDLE,KEY,1)] = self.handle_idle_key
840 self.vstatus.state_table[(STATE_IDLE,DOOR,1)] = self.handle_door_event
841 self.vstatus.state_table[(STATE_IDLE,MIFARE,1)] = self.handle_mifare_event
843 self.vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = self.handle_door_idle
844 self.vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = self.handle_door_event
845 self.vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = self.do_nothing
846 self.vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = self.do_nothing
848 self.vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = self.return_to_idle
849 self.vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = self.handle_door_event
850 self.vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = self.do_nothing
851 self.vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = self.do_nothing
853 self.vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = self.handle_getting_uid_idle
854 self.vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = self.handle_door_event
855 self.vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = self.handle_getting_uid_key
856 self.vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = self.handle_mifare_event
858 self.vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = self.handle_getting_pin_idle
859 self.vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = self.handle_door_event
860 self.vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = self.handle_getting_pin_key
861 self.vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = self.handle_mifare_event
863 self.vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = self.handle_get_selection_idle
864 self.vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = self.handle_door_event
865 self.vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = self.handle_get_selection_key
866 self.vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = self.handle_mifare_add_user_event
868 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = self.handle_idle_grandfather_tick
869 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = self.handle_grandfather_tick
870 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = self.handle_door_event
871 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = self.handle_door_event
872 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = self.do_nothing
873 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = self.do_nothing
874 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = self.handle_mifare_event
877 Get what to do on a state change.
879 def get_state_table_handler(self, state, event, counter):
880 return self.vstatus.state_table[(state,event,counter)]
882 def time_to_next_update(self):
883 idle_update = self.vstatus.time_of_next_idlestep - time()
884 if not self.vstatus.mk.done() and self.vstatus.mk.next_update is not None:
885 mk_update = self.vstatus.mk.next_update - time()
886 if mk_update < idle_update:
887 idle_update = mk_update
890 def run_forever(self, rfh, wfh, options, cf):
891 self.v = VendingMachine(rfh, wfh, USE_MIFARE)
892 self.dispense = Dispense()
893 self.vstatus = VendState(self.v)
894 self.create_state_table()
896 logging.debug('PING is ' + str(self.v.ping()))
902 timeout = self.time_to_next_update()
903 (event, params) = self.v.next_event(timeout)
904 self.run_handler(event, params)
906 def run_handler(self, event, params):
907 handler = self.get_state_table_handler(self.vstatus.state,event,self.vstatus.counter)
909 handler(event, params)
912 Connect to the machine.
914 def connect_to_vend(options, cf):
917 logging.info('Connecting to vending machine using LAT')
918 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
919 rfh, wfh = latclient.get_fh()
920 elif options.use_serial:
921 # Open vending machine via serial.
922 logging.info('Connecting to vending machine using serial')
923 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
924 rfh,wfh = serialclient.get_fh()
926 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
927 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
929 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
930 sock.connect((options.host, options.port))
931 rfh = sock.makefile('r')
932 wfh = sock.makefile('w')
939 Parse arguments from the command line
942 from optparse import OptionParser
944 op = OptionParser(usage="%prog [OPTION]...")
945 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')
946 op.add_option('--serial', action='store_true', default=False, dest='use_serial', help='use the serial port')
947 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
948 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
949 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
950 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
951 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
952 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
953 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
954 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
955 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
956 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
957 options, args = op.parse_args()
960 op.error('extra command line arguments: ' + ' '.join(args))
964 def create_pid_file(name):
966 pid_file = file(name, 'w')
967 pid_file.write('%d\n'%os.getpid())
970 logging.warning('unable to write to pid file '+name+': '+str(e))
973 def do_nothing(signum, stack):
974 signal.signal(signum, do_nothing)
975 def stop_server(signum, stack): raise KeyboardInterrupt
976 signal.signal(signal.SIGHUP, do_nothing)
977 signal.signal(signal.SIGTERM, stop_server)
978 signal.signal(signal.SIGINT, stop_server)
980 options = parse_args()
981 config_opts = VendConfigFile(options.config_file, config_options)
982 if options.daemon: become_daemon()
983 set_up_logging(options)
984 if options.pid_file != '': create_pid_file(options.pid_file)
986 return options, config_opts
988 def clean_up_nicely(options, config_opts):
989 if options.pid_file != '':
991 os.unlink(options.pid_file)
992 logging.debug('Removed pid file '+options.pid_file)
993 except OSError: pass # if we can't delete it, meh
995 def set_up_logging(options):
996 logger = logging.getLogger()
998 if not options.daemon:
999 stderr_logger = logging.StreamHandler(sys.stderr)
1000 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1001 logger.addHandler(stderr_logger)
1003 if options.log_file != '':
1005 file_logger = logging.FileHandler(options.log_file)
1006 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1007 logger.addHandler(file_logger)
1009 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1011 if options.syslog != None:
1012 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1013 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1014 logger.addHandler(sys_logger)
1017 logger.setLevel(logging.WARNING)
1018 elif options.verbose:
1019 logger.setLevel(logging.DEBUG)
1021 logger.setLevel(logging.INFO)
1023 def become_daemon():
1024 dev_null = file('/dev/null')
1025 fd = dev_null.fileno()
1034 raise SystemExit('failed to fork: '+str(e))
1036 def do_vend_server(options, config_opts):
1039 rfh, wfh = connect_to_vend(options, config_opts)
1040 except (SerialClientException, socket.error), e:
1041 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1043 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1044 logging.info("Trying again in 5 seconds.")
1048 # run_forever(rfh, wfh, options, config_opts)
1051 vserver = VendServer()
1052 vserver.run_forever(rfh, wfh, options, config_opts)
1053 except VendingException:
1054 logging.error("Connection died, trying again...")
1055 logging.info("Trying again in 5 seconds.")
1059 def main(argv=None):
1060 options, config_opts = set_stuff_up()
1063 logging.warning('Starting Vend Server')
1064 do_vend_server(options, config_opts)
1065 logging.error('Vend Server finished unexpectedly, restarting')
1066 except KeyboardInterrupt:
1067 logging.info("Killed by signal, cleaning up")
1068 clean_up_nicely(options, config_opts)
1069 logging.warning("Vend Server stopped")
1074 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1075 tb = format_tb(exc_traceback, 20)
1078 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1079 logging.critical("Message: " + str(exc_value))
1080 logging.critical("Traceback:")
1082 for line in event.split('\n'):
1083 logging.critical(' '+line)
1084 logging.critical("This message should be considered a bug in the Vend Server.")
1085 logging.critical("Please report this to someone who can fix it.")
1087 logging.warning("Trying again anyway (might not help, but hey...)")
1089 if __name__ == '__main__':