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 msg = [(self.center('WELCOME'), False, TEXT_SPEED),
144 (self.center(self.dispense.getUsername()), False, TEXT_SPEED),
145 (self.center(balance), False, TEXT_SPEED),]
148 choices = ' '*10+'CHOICES: '
150 # Show what is in the coke machine
151 # Need to update this so it uses the abstracted system
153 for i in ['08', '18', '28', '38', '48', '58', '68']:
154 cokes.append((i, self.dispense.getItemInfo(i)))
157 if c[1][0] == 'dead':
159 choices += '%s-(%sc)-%s8 '%(c[1][0], c[1][1], c[0])
161 # Show the final few options
162 choices += '55-DOOR '
163 choices += 'OR ANOTHER SNACK. '
164 choices += '99 TO READ AGAIN. '
165 choices += 'CHOICE? '
166 msg.append((choices, False, None))
167 # Send it to the display
171 In here just for fun.
175 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
176 choice = int(random()*len(messages))
177 msg = messages[choice]
178 left = range(len(msg))
179 for i in range(len(msg)):
180 if msg[i] == ' ': left.remove(i)
184 for i in range(0, len(msg)):
190 s += chr(int(random()*26)+ord('A'))
198 Format text so it will appear centered on the screen.
200 def center(self, str):
202 return ' '*((LEN-len(str))/2)+str
205 Configure the things that will appear on screen whil the machine is idling.
207 def setup_idlers(self):
211 GrayIdler(self.v,one="*",zero="-"),
212 GrayIdler(self.v,one="/",zero="\\"),
213 GrayIdler(self.v,one="X",zero="O"),
214 GrayIdler(self.v,one="*",zero="-",reorder=1),
215 GrayIdler(self.v,one="/",zero="\\",reorder=1),
216 GrayIdler(self.v,one="X",zero="O",reorder=1),
222 StringIdler(self.v), # Hello Cruel World
223 StringIdler(self.v, text="Kill 'em all", repeat=False),
224 StringIdler(self.v, text=CREDITS),
225 StringIdler(self.v, text=str(math.pi) + " "),
226 StringIdler(self.v, text=str(math.e) + " "),
227 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),
228 # "Hello World" in brainfuck
229 StringIdler(self.v, text=">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++."),
233 FileIdler(self.v, '/usr/share/common-licenses/GPL-2',affinity=2),
235 PipeIdler(self.v, "/usr/bin/getent", "passwd"),
236 FortuneIdler(self.v,affinity=20),
242 Go back to the default idler.
244 def reset_idler(self, t = None):
245 self.idler = GreetingIdler(self.v, t)
246 self.vstatus.time_of_next_idlestep = time()+self.idler.next()
247 self.vstatus.time_of_next_idler = None
248 self.vstatus.time_to_autologout = None
249 self.vstatus.change_state(STATE_IDLE, 1)
252 Change to a random idler.
254 def choose_idler(self):
256 # Implementation of the King Of the Hill algorithm from;
257 # http://eli.thegreenplace.net/2010/01/22/weighted-random-generation-in-python/
259 #def weighted_choice_king(weights):
262 # for i, w in enumerate(weights):
264 # if random.random() * total < w:
272 for choice in self.idlers:
273 weight = choice.affinity()
275 if random() * total < weight:
283 Run every step while the machine is idling.
286 if self.idler.finished():
288 self.vstatus.time_of_next_idler = time() + 30
289 nextidle = self.idler.next()
291 nextidle = IDLE_SPEED
292 self.vstatus.time_of_next_idlestep = time()+nextidle
295 These next two events trigger no response in the code.
297 def handle_tick_event(self, event, params):
298 # don't care right now.
301 def handle_switch_event(self, event, params):
302 # don't care right now.
306 Don't do anything for this event.
308 def do_nothing(self, event, params):
309 print "doing nothing (s,e,p)", state, " ", event, " ", params
313 These next few entrie tell us to do nothing while we are idling
315 def handle_getting_uid_idle(self, event, params):
316 # don't care right now.
319 def handle_getting_pin_idle(self, event, params):
320 # don't care right now.
324 While logged in and waiting for user input, slowly get closer to logging out.
326 def handle_get_selection_idle(self, event, params):
327 # don't care right now.
329 ### State logging out ..
330 if self.vstatus.time_to_autologout != None:
331 time_left = self.vstatus.time_to_autologout - time()
332 if time_left < 6 and (self.vstatus.last_timeout_refresh is None or self.vstatus.last_timeout_refresh > time_left):
333 self.vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
334 self.vstatus.last_timeout_refresh = int(time_left)
335 self.vstatus.cur_selection = ''
337 # Login timed out: Log out the current user.
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
344 self.dispense.logOut()
347 ### State fully logged out ... reset variables
348 if self.vstatus.time_to_autologout and not self.vstatus.mk.done():
349 self.vstatus.time_to_autologout = None
350 if self.vstatus.cur_user == '' and self.vstatus.time_to_autologout:
351 self.vstatus.time_to_autologout = None
354 if len(self.vstatus.cur_pin) == PIN_LENGTH and self.vstatus.mk.done() and self.vstatus.time_to_autologout == None:
356 self.vstatus.time_to_autologout = time() + 15
357 self.vstatus.last_timeout_refresh = None
359 ## FIXME - this may need to be elsewhere.....
361 self.vstatus.mk.update_display()
364 Triggered on user input while logged in.
366 def handle_get_selection_key(self, event, params):
368 if len(self.vstatus.cur_selection) == 0:
370 self.vstatus.cur_pin = ''
371 self.vstatus.cur_user = ''
372 self.vstatus.cur_selection = ''
373 self._last_card_id = -1
374 self.dispense.logOut()
375 self.vstatus.mk.set_messages([(self.center('BYE!'), False, 1.5)])
378 self.vstatus.cur_selection += chr(key + ord('0'))
379 self.vstatus.mk.set_message('SELECT: '+self.vstatus.cur_selection)
380 self.vstatus.time_to_autologout = None
381 elif len(self.vstatus.cur_selection) == 1:
383 self.vstatus.cur_selection = ''
384 self.vstatus.time_to_autologout = None
385 self.dispense.logOut()
386 self.scroll_options(self.vstatus.username, self.vstatus.mk)
389 self.vstatus.cur_selection += chr(key + ord('0'))
390 if self.dispense.isLoggedIn():
391 self.make_selection()
392 self.vstatus.cur_selection = ''
393 self.vstatus.time_to_autologout = time() + 8
394 self.vstatus.last_timeout_refresh = None
397 (name,price) = self.dispense.getItemInfo(self.vstatus.cur_selection)
398 dollarprice = "$%.2f" % ( price / 100.0 )
399 self.v.display( self.vstatus.cur_selection+' - %s'%dollarprice)
401 self.vstatus.cur_selection = ''
402 self.vstatus.time_to_autologout = None
403 self.vstatus.last_timeout_refresh = None
406 Triggered when the user has entered the id of something they would like to purchase.
408 def make_selection(self):
409 logging.debug('Dispense item "%s"' % (self.vstatus.cur_selection,))
410 # should use sudo here
411 if self.vstatus.cur_selection == '55':
412 self.vstatus.mk.set_message('OPENSESAME')
413 logging.info('dispensing a door for %s'%self.vstatus.username)
415 ret = os.system('dispense -u "%s" door'%self.vstatus.username)
417 ret = os.system('dispense door')
419 logging.info('door opened')
420 self.vstatus.mk.set_message(self.center('DOOR OPEN'))
422 logging.warning('user %s tried to dispense a bad door'%self.vstatus.username)
423 self.vstatus.mk.set_message(self.center('BAD DOOR'))
425 elif self.vstatus.cur_selection == '81':
427 elif self.vstatus.cur_selection == '99':
428 self.scroll_options(self.vstatus.username, self.vstatus.mk)
429 self.vstatus.cur_selection = ''
431 elif self.vstatus.cur_selection[1] == '8':
432 self.v.display('GOT DRINK?')
433 if ((os.system('dispense -u "%s" coke:%s'%(self.vstatus.username, self.vstatus.cur_selection[0])) >> 8) != 0):
434 self.v.display('SEEMS NOT')
436 self.v.display('GOT DRINK!')
438 # first see if it's a named slot
440 price, shortname, name = get_snack( self.vstatus.cur_selection )
442 price, shortname, name = get_snack( '--' )
443 dollarprice = "$%.2f" % ( price / 100.0 )
444 self.v.display(self.vstatus.cur_selection+' - %s'%dollarprice)
445 exitcode = os.system('dispense -u "%s" snack:%s'%(self.vstatus.username, self.vstatus.cur_selection)) >> 8
447 # magic dispense syslog service
448 (worked, code, string) = self.v.vend(self.vstatus.cur_selection)
450 self.v.display('THANK YOU')
451 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, self.vstatus.cur_selection, self.vstatus.username))
453 print "Vend Failed:", code, string
454 syslog.syslog(syslog.LOG_WARNING | syslog.LOG_LOCAL4, "vending %s (slot %s) for %s FAILED %r %r" % (name, self.vstatus.cur_selection, self.vstatus.username, code, string))
455 self.v.display('VEND FAIL')
456 elif (exitcode == 5): # RV_BALANCE
457 self.v.display('NO MONEY?')
458 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
459 self.v.display('EMPTY SLOT')
460 elif (exitcode == 1): # RV_BADITEM (Dead slot)
461 self.v.display('EMPTY SLOT')
463 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))
464 self.v.display('UNK ERROR')
468 Triggered when the user presses a button while entering their pin.
470 def handle_getting_pin_key(self, event, params):
472 if len(self.vstatus.cur_pin) < PIN_LENGTH:
474 if self.vstatus.cur_pin == '':
475 self.vstatus.cur_user = ''
476 self.dispense.logOut()
480 self.vstatus.cur_pin = ''
481 self.vstatus.mk.set_message('PIN: ')
483 self.vstatus.cur_pin += chr(key + ord('0'))
484 self.vstatus.mk.set_message('PIN: '+'X'*len(self.vstatus.cur_pin))
485 if len(self.vstatus.cur_pin) == PIN_LENGTH:
486 if self.dispense.authUserIdPin(self.vstatus.cur_user, self.vstatus.cur_pin):
487 self.vstatus.username = self.dispense.getUsername()
488 self.v.beep(0, False)
489 self.vstatus.cur_selection = ''
490 self.vstatus.change_state(STATE_GET_SELECTION)
491 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
494 self.v.beep(40, False)
495 self.vstatus.mk.set_messages(
496 [(self.center('BAD PIN'), False, 1.0),
497 (self.center('SORRY'), False, 0.5)])
498 self.vstatus.cur_user = ''
499 self.vstatus.cur_pin = ''
506 Triggered when the user presses a button while entering their user id.
508 def handle_getting_uid_key(self, event, params):
510 # complicated key handling here:
512 if len(self.vstatus.cur_user) == 0 and key == 9:
513 self.vstatus.cur_selection = ''
514 self.vstatus.time_to_autologout = None
515 self.vstatus.mk.set_message('PRICECHECK')
517 self.scroll_options('', self.vstatus.mk)
518 self.vstatus.change_state(STATE_GET_SELECTION)
521 if len(self.vstatus.cur_user) <8:
523 self.vstatus.cur_user = ''
524 self.dispense.logOut()
528 self.vstatus.cur_user += chr(key + ord('0'))
529 #logging.info('dob: '+vstatus.cur_user)
530 if len(self.vstatus.cur_user) > 5:
531 self.vstatus.mk.set_message('>'+self.vstatus.cur_user)
533 self.vstatus.mk.set_message('UID: '+self.vstatus.cur_user)
535 if len(self.vstatus.cur_user) == 5:
536 uid = int(self.vstatus.cur_user)
539 logging.info('user '+self.vstatus.cur_user+' has a bad PIN')
545 Welcome to Picklevision Sytems, Sunnyvale, CA
547 Greetings Professor Falken.
552 Shall we play a game?
555 Please choose from the following menu:
562 6. Toxic and Biochemical Warfare
563 7. Global Thermonuclear War
567 Wouldn't you prefer a nice game of chess?
569 """.replace('\n',' ')
570 self.vstatus.mk.set_messages([(pfalken, False, 10)])
571 self.vstatus.cur_user = ''
572 self.vstatus.cur_pin = ''
578 # TODO Fix this up do we can check before logging in
580 if self.dispense.isDisabled():
581 logging.info('user '+self.vstatus.cur_user+' is disabled')
582 self.vstatus.mk.set_messages(
583 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
584 self.vstatus.cur_user = ''
585 self.vstatus.cur_pin = ''
591 self.vstatus.cur_pin = ''
592 self.vstatus.mk.set_message('PIN: ')
593 logging.info('need pin for user %s'%self.vstatus.cur_user)
594 self.vstatus.change_state(STATE_GETTING_PIN)
598 Triggered when a key is pressed and the machine is idling.
600 def handle_idle_key(self, event, params):
603 self.vstatus.cur_user = ''
604 self.dispense.logOut()
608 self.vstatus.change_state(STATE_GETTING_UID)
609 self.run_handler(event, params)
612 What to do when there is nothing to do.
614 def handle_idle_tick(self, event, params):
616 if self.vstatus.mk.done():
619 if self.vstatus.time_of_next_idler and time() > self.vstatus.time_of_next_idler:
620 self.vstatus.time_of_next_idler = time() + 30
625 self.vstatus.mk.update_display()
627 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK)
628 self.run_handler(event, params)
632 Manages the beeps for the grandfather clock
634 def beep_on(self, when, before=0):
635 start = int(when - before)
639 if now >= start and now <= end:
643 def handle_idle_grandfather_tick(self, event, params):
644 ### check for interesting times
647 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
648 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
649 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
650 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
652 hourfromnow = localtime(time() + 3600)
654 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
655 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
656 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
658 ## check for X seconds to the hour
659 ## if case, update counter to 2
660 if self.beep_on(onthehour,15) \
661 or self.beep_on(halfhour,0) \
662 or self.beep_on(quarterhour,0) \
663 or self.beep_on(threequarterhour,0) \
664 or self.beep_on(fivetothehour,0):
665 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
666 self.run_handler(event, params)
668 self.vstatus.change_state(STATE_IDLE)
670 def handle_grandfather_tick(self, event, params):
674 ### we live in interesting times
677 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
678 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
679 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
680 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
682 hourfromnow = localtime(time() + 3600)
684 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
685 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
686 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
689 #print "when it fashionable to wear a onion on your hip"
691 if self.beep_on(onthehour,15):
693 next_hour=((hourfromnow[3] + 11) % 12) + 1
694 if onthehour - time() < next_hour and onthehour - time() > 0:
695 self.v.beep(0, False)
699 msg.append(("DING!", False, None))
701 msg.append((" DING!", False, None))
702 elif int(onthehour - time()) == 0:
703 self.v.beep(255, False)
704 msg.append((" BONG!", False, None))
705 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
706 elif self.beep_on(halfhour,0):
708 self.v.beep(0, False)
709 msg.append((" HALFHOUR ", False, 50))
710 elif self.beep_on(quarterhour,0):
712 self.v.beep(0, False)
713 msg.append((" QTR HOUR ", False, 50))
714 elif self.beep_on(threequarterhour,0):
716 self.v.beep(0, False)
717 msg.append((" 3 QTR HR ", False, 50))
718 elif self.beep_on(fivetothehour,0):
720 self.v.beep(0, False)
721 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
725 ## check for X seconds to the hour
728 self.vstatus.mk.set_messages(msg)
731 self.vstatus.mk.update_display()
732 ## if no longer case, return to idle
734 ## change idler to be clock
735 if go_idle and self.vstatus.mk.done():
736 self.vstatus.change_state(STATE_IDLE,1)
739 What to do when the door is open.
741 def handle_door_idle(self, event, params):
742 def twiddle(clock,v,wise = 2):
744 v.display("-FEED ME-")
745 elif (clock % 4 == 1+wise):
746 v.display("\\FEED ME/")
747 elif (clock % 4 == 2):
748 v.display("-FEED ME-")
749 elif (clock % 4 == 3-wise):
750 v.display("/FEED ME\\")
752 # don't care right now.
755 if ((now % 60 % 2) == 0):
758 twiddle(now, self.v, wise=0)
761 What to do when the door is opened or closed.
763 def handle_door_event(self, event, params):
764 if params == 0: #door open
765 self.vstatus.change_state(STATE_DOOR_OPENING)
766 logging.warning("Entering open door mode")
767 self.v.display("-FEED ME-")
769 self.dispense.logOut()
770 self.vstatus.cur_user = ''
771 self.vstatus.cur_pin = ''
772 elif params == 1: #door closed
773 self.vstatus.change_state(STATE_DOOR_CLOSING)
776 logging.warning('Leaving open door mode')
777 self.v.display("-YUM YUM!-")
780 Triggered when a user swipes their caed, and the machine is logged out.
782 def handle_mifare_event(self, event, params):
784 # Translate card_id into uid.
785 if card_id == None or card_id == self._last_card_id:
788 self._last_card_id = card_id
790 if not self.dispense.authMifareCard(card_id):
791 self.v.beep(40, False)
792 self.vstatus.mk.set_messages(
793 [(self.center('BAD CARD'), False, 1.0),
794 (self.center('SORRY'), False, 0.5)])
795 self.vstatus.cur_user = ''
796 self.vstatus.cur_pin = ''
797 self._last_card_id = -1
801 elif self.dispense.isDisabled():
802 logging.info('Mapped card id to uid %s'%self.dispense.getUsername())
803 self.v.beep(40, False)
804 self.vstatus.mk.set_messages(
805 [(self.center('ACCT DISABLED'), False, 1.0),
806 (self.center('SORRY'), False, 0.5)])
807 self.dispense.logOut()
811 logging.info('Mapped card id to uid %s'%self.dispense.getUsername())
812 self.vstatus.cur_user = '----'
813 self.vstatus.username = self.dispense.getUsername()
814 self.vstatus.cur_selection = ''
815 self.vstatus.change_state(STATE_GET_SELECTION)
816 self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
820 Triggered when a user swipes their card and the machine is logged in.
822 def handle_mifare_add_user_event(self, event, params):
825 # Translate card_id into uid.
826 if card_id == None or card_id == self._last_card_id:
829 self._last_card_id = card_id
831 if not self.dispense.addCard(card_id):
832 self.vstatus.mk.set_messages(
833 [(self.center('ALREADY'), False, 0.5),
834 (self.center('ENROLLED'), False, 0.5)])
836 self.vstatus.mk.set_messages(
837 [(self.center('CARD'), False, 0.5),
838 (self.center('ENROLLED'), False, 0.5)])
840 def return_to_idle(self, event, params):
844 Maps what to do when the state changes.
846 def create_state_table(self):
847 self.vstatus.state_table[(STATE_IDLE,TICK,1)] = self.handle_idle_tick
848 self.vstatus.state_table[(STATE_IDLE,KEY,1)] = self.handle_idle_key
849 self.vstatus.state_table[(STATE_IDLE,DOOR,1)] = self.handle_door_event
850 self.vstatus.state_table[(STATE_IDLE,MIFARE,1)] = self.handle_mifare_event
852 self.vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = self.handle_door_idle
853 self.vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = self.handle_door_event
854 self.vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = self.do_nothing
855 self.vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = self.do_nothing
857 self.vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = self.return_to_idle
858 self.vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = self.handle_door_event
859 self.vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = self.do_nothing
860 self.vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = self.do_nothing
862 self.vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = self.handle_getting_uid_idle
863 self.vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = self.handle_door_event
864 self.vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = self.handle_getting_uid_key
865 self.vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = self.handle_mifare_event
867 self.vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = self.handle_getting_pin_idle
868 self.vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = self.handle_door_event
869 self.vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = self.handle_getting_pin_key
870 self.vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = self.handle_mifare_event
872 self.vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = self.handle_get_selection_idle
873 self.vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = self.handle_door_event
874 self.vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = self.handle_get_selection_key
875 self.vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = self.handle_mifare_add_user_event
877 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = self.handle_idle_grandfather_tick
878 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = self.handle_grandfather_tick
879 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = self.handle_door_event
880 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = self.handle_door_event
881 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = self.do_nothing
882 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = self.do_nothing
883 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = self.handle_mifare_event
886 Get what to do on a state change.
888 def get_state_table_handler(self, state, event, counter):
889 return self.vstatus.state_table[(state,event,counter)]
891 def time_to_next_update(self):
892 idle_update = self.vstatus.time_of_next_idlestep - time()
893 if not self.vstatus.mk.done() and self.vstatus.mk.next_update is not None:
894 mk_update = self.vstatus.mk.next_update - time()
895 if mk_update < idle_update:
896 idle_update = mk_update
899 def run_forever(self, rfh, wfh, options, cf):
900 self.v = VendingMachine(rfh, wfh, USE_MIFARE)
901 self.dispense = Dispense()
902 self.vstatus = VendState(self.v)
903 self.create_state_table()
905 logging.debug('PING is ' + str(self.v.ping()))
911 timeout = self.time_to_next_update()
912 (event, params) = self.v.next_event(timeout)
913 self.run_handler(event, params)
915 def run_handler(self, event, params):
916 handler = self.get_state_table_handler(self.vstatus.state,event,self.vstatus.counter)
918 handler(event, params)
921 Connect to the machine.
923 def connect_to_vend(options, cf):
926 logging.info('Connecting to vending machine using LAT')
927 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
928 rfh, wfh = latclient.get_fh()
929 elif options.use_serial:
930 # Open vending machine via serial.
931 logging.info('Connecting to vending machine using serial')
932 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
933 rfh,wfh = serialclient.get_fh()
935 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
936 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
938 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
939 sock.connect((options.host, options.port))
940 rfh = sock.makefile('r')
941 wfh = sock.makefile('w')
948 Parse arguments from the command line
951 from optparse import OptionParser
953 op = OptionParser(usage="%prog [OPTION]...")
954 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')
955 op.add_option('--serial', action='store_true', default=False, dest='use_serial', help='use the serial port')
956 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
957 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
958 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
959 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
960 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
961 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
962 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
963 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
964 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
965 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
966 options, args = op.parse_args()
969 op.error('extra command line arguments: ' + ' '.join(args))
973 def create_pid_file(name):
975 pid_file = file(name, 'w')
976 pid_file.write('%d\n'%os.getpid())
979 logging.warning('unable to write to pid file '+name+': '+str(e))
982 def do_nothing(signum, stack):
983 signal.signal(signum, do_nothing)
984 def stop_server(signum, stack): raise KeyboardInterrupt
985 signal.signal(signal.SIGHUP, do_nothing)
986 signal.signal(signal.SIGTERM, stop_server)
987 signal.signal(signal.SIGINT, stop_server)
989 options = parse_args()
990 config_opts = VendConfigFile(options.config_file, config_options)
991 if options.daemon: become_daemon()
992 set_up_logging(options)
993 if options.pid_file != '': create_pid_file(options.pid_file)
995 return options, config_opts
997 def clean_up_nicely(options, config_opts):
998 if options.pid_file != '':
1000 os.unlink(options.pid_file)
1001 logging.debug('Removed pid file '+options.pid_file)
1002 except OSError: pass # if we can't delete it, meh
1004 def set_up_logging(options):
1005 logger = logging.getLogger()
1007 if not options.daemon:
1008 stderr_logger = logging.StreamHandler(sys.stderr)
1009 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1010 logger.addHandler(stderr_logger)
1012 if options.log_file != '':
1014 file_logger = logging.FileHandler(options.log_file)
1015 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1016 logger.addHandler(file_logger)
1018 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1020 if options.syslog != None:
1021 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1022 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1023 logger.addHandler(sys_logger)
1026 logger.setLevel(logging.WARNING)
1027 elif options.verbose:
1028 logger.setLevel(logging.DEBUG)
1030 logger.setLevel(logging.INFO)
1032 def become_daemon():
1033 dev_null = file('/dev/null')
1034 fd = dev_null.fileno()
1043 raise SystemExit('failed to fork: '+str(e))
1045 def do_vend_server(options, config_opts):
1048 rfh, wfh = connect_to_vend(options, config_opts)
1049 except (SerialClientException, socket.error), e:
1050 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1052 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1053 logging.info("Trying again in 5 seconds.")
1057 # run_forever(rfh, wfh, options, config_opts)
1060 vserver = VendServer()
1061 vserver.run_forever(rfh, wfh, options, config_opts)
1062 except VendingException:
1063 logging.error("Connection died, trying again...")
1064 logging.info("Trying again in 5 seconds.")
1068 def main(argv=None):
1069 options, config_opts = set_stuff_up()
1072 logging.warning('Starting Vend Server')
1073 do_vend_server(options, config_opts)
1074 logging.error('Vend Server finished unexpectedly, restarting')
1075 except KeyboardInterrupt:
1076 logging.info("Killed by signal, cleaning up")
1077 clean_up_nicely(options, config_opts)
1078 logging.warning("Vend Server stopped")
1083 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1084 tb = format_tb(exc_traceback, 20)
1087 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1088 logging.critical("Message: " + str(exc_value))
1089 logging.critical("Traceback:")
1091 for line in event.split('\n'):
1092 logging.critical(' '+line)
1093 logging.critical("This message should be considered a bug in the Vend Server.")
1094 logging.critical("Please report this to someone who can fix it.")
1096 logging.warning("Trying again anyway (might not help, but hey...)")
1098 if __name__ == '__main__':