chmod pin automatically. remove boot up message now it doesnt take so long to boot.
[zanchey/dispense2.git] / sql-edition / servers / VendServer.py
index 4e0b16c..22958bd 100755 (executable)
@@ -4,17 +4,37 @@
 USE_DB = 0
 
 import ConfigParser
-import sys, os, string, re, pwd
+import sys, os, string, re, pwd, signal, math
+import logging, logging.handlers
+from traceback import format_tb
 if USE_DB: import pg
-from time import time, sleep
+from time import time, sleep, mktime, localtime
 from popen2 import popen2
 from LATClient import LATClient, LATClientException
+from SerialClient import SerialClient, SerialClientException
 from VendingMachine import VendingMachine, VendingException
+from MessageKeeper import MessageKeeper
 from HorizScroll import HorizScroll
 from random import random, seed
-from Idler import TrainIdler,GrayIdler
+from Idler import TrainIdler,GrayIdler,StringIdler,ClockIdler,FortuneIdler,FileIdler,PipeIdler
 import socket
-from traceback import print_tb
+from posix import geteuid
+
+CREDITS="""
+This vending machine software brought to you by:
+Bernard Blackham
+Mark Tearle
+Nick Bannon
+Cameron Patrick
+and a collective of hungry alpacas.
+
+
+
+For a good time call +61 8 6488 3901
+
+
+
+"""
 
 GREETING = 'UCC SNACKS'
 PIN_LENGTH = 4
@@ -22,6 +42,21 @@ PIN_LENGTH = 4
 DOOR = 1
 SWITCH = 2
 KEY = 3
+TICK = 4
+
+
+STATE_IDLE = 1
+STATE_DOOR_OPENING = 2
+STATE_DOOR_CLOSING = 3
+STATE_GETTING_UID = 4
+STATE_GETTING_PIN = 5
+STATE_GET_SELECTION = 6
+STATE_GRANDFATHER_CLOCK = 7
+
+TEXT_SPEED = 0.8
+IDLE_SPEED = 0.02
+
+class DispenseDatabaseException(Exception): pass
 
 class DispenseDatabase:
        def __init__(self, vending_machine, host, name, user, password):
@@ -30,16 +65,15 @@ class DispenseDatabase:
                self.db.query('LISTEN vend_requests')
 
        def process_requests(self):
-               print 'processing'
+               logging.debug('database processing')
                query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
                try:
                        outstanding = self.db.query(query).getresult()
                except (pg.error,), db_err:
-                       sys.stderr.write('Failed to query database: %s\n'%(db_err.strip()))
-                       return
+                       raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
                for (id, slot) in outstanding:
                        (worked, code, string) = self.vending_machine.vend(slot)
-                       print (worked, code, string)
+                       logging.debug (str((worked, code, string)))
                        if worked:
                                query = 'SELECT vend_success(%s)'%id
                                self.db.query(query).getresult()
@@ -55,8 +89,8 @@ class DispenseDatabase:
 
 def scroll_options(username, mk, welcome = False):
        if welcome:
-               msg = [(center('WELCOME'), False, 0.8),
-                          (center(username), False, 0.8)]
+               msg = [(center('WELCOME'), False, TEXT_SPEED),
+                          (center(username), False, TEXT_SPEED)]
        else:
                msg = []
        choices = ' '*10+'CHOICES: '
@@ -83,22 +117,27 @@ def get_pin(uid):
        try:
                info = pwd.getpwuid(uid)
        except KeyError:
+               logging.info('getting pin for uid %d: user not in password file'%uid)
                return None
        if info.pw_dir == None: return False
        pinfile = os.path.join(info.pw_dir, '.pin')
        try:
                s = os.stat(pinfile)
        except OSError:
+               logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
                return None
        if s.st_mode & 077:
-               return None
+               logging.info('getting pin for uid %d: .pin has wrong permissions. Fixing.'%uid)
+               os.chmod(pinfile, 0600)
        try:
                f = file(pinfile)
        except IOError:
+               logging.info('getting pin for uid %d: I cannot read pin file'%uid)
                return None
        pinstr = f.readline()
        f.close()
        if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
+               logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
                return None
        return int(pinstr)
 
@@ -108,22 +147,12 @@ def has_good_pin(uid):
 def verify_user_pin(uid, pin):
        if get_pin(uid) == pin:
                info = pwd.getpwuid(uid)
+               logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
                return info.pw_name
        else:
+               logging.info('refused pin for uid %d'%(uid))
                return None
 
-def door_open_mode(v):
-       print "Entering open door mode"
-       v.display("-FEED  ME-")
-       while True:
-               e = v.next_event()
-               if e == None: break
-               (event, params) = e
-               if event == DOOR:
-                       if params == 1: # door closed
-                               v.display("-YUM YUM!-")
-                               sleep(1)
-                               return
 
 def cookie(v):
        seed(time())
@@ -153,224 +182,586 @@ def center(str):
        LEN = 10
        return ' '*((LEN-len(str))/2)+str
 
-class MessageKeeper:
-       def __init__(self, vendie):
-               # Each element of scrolling_message should be a 3-tuple of
-               # ('message', True/False if it is to be repeated, time to display)
-               self.scrolling_message = []
-               self.v = vendie
-               self.next_update = None
 
-       def set_message(self, string):
-               self.scrolling_message = [(string, False, None)]
-               self.update_display(True)
 
-       def set_messages(self, strings):
-               self.scrolling_message = strings
-               self.update_display(True)
+idlers = []
+idler = None
+
+def setup_idlers(v):
+       global idlers, idler
+       idlers = [
+                GrayIdler(v),
+               StringIdler(v, text="Kill 'em all", repeat=False),
+                GrayIdler(v,one="*",zero="-"),
+               StringIdler(v, text=CREDITS),
+                GrayIdler(v,one="/",zero="\\"),
+               ClockIdler(v),
+                GrayIdler(v,one="X",zero="O"),
+               FileIdler(v, '/usr/share/common-licenses/GPL-2'),
+                GrayIdler(v,one="*",zero="-",reorder=1),
+               StringIdler(v, text=str(math.pi) + "            "),
+               ClockIdler(v),
+                GrayIdler(v,one="/",zero="\\",reorder=1),
+               StringIdler(v, text=str(math.e) + "            "),
+                GrayIdler(v,one="X",zero="O",reorder=1),
+               StringIdler(v, text="    I want some pizza - please call Pizza Hut Shenton Park on +61 8 9381 9979 - and order as Quinn - I am getting really hungry", repeat=False),
+               PipeIdler(v, "/usr/bin/ypcat", "passwd"),
+               FortuneIdler(v),
+               ClockIdler(v),
+               StringIdler(v),
+               TrainIdler(v),
+               ]
+       disabled = [
+               ]
+       idler = choose_idler()
+
+def choose_idler():
+       global idlers, idler
+       iiindex = 0
+
+       if idler:
+               iiindex = idlers.index(idler)
+
+       iilen = len(idlers)
+
+       move = int(random()*len(idlers)) + 1
+
+       while move >= 0:
+               idler = idlers[( (iiindex + 1) % iilen)]
+               move = move - idler.affinity()
+
+       idler.reset()
+
+def idle_step():
+       global idler
+       if idler.finished():
+               choose_idler()
+       sleep(IDLE_SPEED)
+       idler.next()
+
+class VendState:
+       def __init__(self,v):
+               self.state_table = {}
+               self.state = STATE_IDLE
+               self.counter = 0
+
+               self.mk = MessageKeeper(v)
+               self.cur_user = ''
+               self.cur_pin = ''
+               self.username = ''
+               self.cur_selection = ''
+               self.time_to_autologout = None
+
+               self.time_to_idle = None
+
+               self.last_timeout_refresh = None
+
+       def change_state(self,newstate,newcounter=None):
+               if self.state != newstate:
+                       #print "Changing state from: ", 
+                       #print self.state,
+                       #print " to ", 
+                       #print newstate 
+                       self.state = newstate
+
+               if newcounter is not None and self.counter != newcounter:
+                       #print "Changing counter from: ", 
+                       #print self.counter,
+                       #print " to ", 
+                       #print newcounter 
+                       self.counter = newcounter
+
+
+
+def handle_tick_event(event, params, v, vstatus):
+       # don't care right now.
+       pass
+
+def handle_switch_event(event, params, v, vstatus):
+       # don't care right now.
+       pass
+
+
+def do_nothing(state, event, params, v, vstatus):
+       print "doing nothing (s,e,p)", state, " ", event, " ", params
+       pass
+
+def handle_getting_uid_idle(state, event, params, v, vstatus):
+       # don't care right now.
+       pass
+
+def handle_getting_pin_idle(state, event, params, v, vstatus):
+       # don't care right now.
+       pass
+
+def handle_get_selection_idle(state, event, params, v, vstatus):
+       # don't care right now.
+       ###
+       ### State logging out ..
+       if vstatus.time_to_autologout != None:
+               time_left = vstatus.time_to_autologout - time()
+               if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
+                       vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
+                       vstatus.last_timeout_refresh = int(time_left)
+                       vstatus.cur_selection = ''
+
+       if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
+               vstatus.time_to_autologout = None
+               vstatus.cur_user = ''
+               vstatus.cur_pin = ''
+               vstatus.cur_selection = ''
+                       
+               idle_in(vstatus,2)
+               vstatus.change_state(STATE_IDLE)
+
+               vstatus.mk.set_message(GREETING)
+
+       ### State fully logged out ... reset variables
+       if vstatus.time_to_autologout and not vstatus.mk.done(): 
+               vstatus.time_to_autologout = None
+       if vstatus.cur_user == '' and vstatus.time_to_autologout: 
+               vstatus.time_to_autologout = None
+       
+       ### State logged in
+       if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
+               # start autologout
+               vstatus.time_to_autologout = time() + 15
+               vstatus.last_timeout_refresh = None
+
+       ### State logged out ... after normal logout??
+       # perhaps when logged in?
+       if vstatus.time_to_idle is not None and vstatus.cur_user != '': 
+               vstatus.time_to_idle = None
+
+
+       ## FIXME - this may need to be elsewhere.....
+       # need to check
+       vstatus.mk.update_display()
+
+
+
+def handle_get_selection_key(state, event, params, v, vstatus):
+       key = params
+       if len(vstatus.cur_selection) == 0:
+               if key == 11:
+                       vstatus.cur_pin = ''
+                       vstatus.cur_user = ''
+                       vstatus.cur_selection = ''
+                       
+                       idle_in(vstatus,2)
+                       vstatus.change_state(STATE_IDLE)
+
+                       vstatus.mk.set_messages(
+                               [(center('BYE!'), False, 1.5),
+                                (GREETING, False, None)])
+                       return
+               vstatus.cur_selection += chr(key + ord('0'))
+               vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
+               vstatus.time_to_autologout = None
+       elif len(vstatus.cur_selection) == 1:
+               if key == 11:
+                       vstatus.cur_selection = ''
+                       vstatus.time_to_autologout = None
+                       scroll_options(vstatus.username, vstatus.mk)
+                       return
+               else:
+                       vstatus.cur_selection += chr(key + ord('0'))
+                       make_selection(v,vstatus)
+                       vstatus.cur_selection = ''
+                       vstatus.time_to_autologout = time() + 8
+                       vstatus.last_timeout_refresh = None
+
+def make_selection(v, vstatus):
+       # should use sudo here
+       if vstatus.cur_selection == '55':
+               vstatus.mk.set_message('OPENSESAME')
+               logging.info('dispensing a door for %s'%vstatus.username)
+               if geteuid() == 0:
+                       ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
+               else:
+                       ret = os.system('dispense door')
+               if ret == 0:
+                       logging.info('door opened')
+                       vstatus.mk.set_message(center('DOOR OPEN'))
+               else:
+                       logging.warning('user %s tried to dispense a bad door'%vstatus.username)
+                       vstatus.mk.set_message(center('BAD DOOR'))
+               sleep(1)
+       elif vstatus.cur_selection == '91':
+               cookie(v)
+       elif vstatus.cur_selection == '99':
+               scroll_options(vstatus.username, vstatus.mk)
+               vstatus.cur_selection = ''
+               return
+       elif vstatus.cur_selection[1] == '8':
+               v.display('GOT COKE?')
+               if ((os.system('su - "%s" -c "dispense %s"'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
+                       v.display('SEEMS NOT')
+               else:
+                       v.display('GOT COKE!')
+       else:
+               v.display(vstatus.cur_selection+' - $1.00')
+               if ((os.system('su - "%s" -c "dispense snack"'%(vstatus.username)) >> 8) == 0):
+                       v.vend(vstatus.cur_selection)
+                       v.display('THANK YOU')
+               else:
+                       v.display('NO MONEY?')
+       sleep(1)
+
+
+def handle_getting_pin_key(state, event, params, v, vstatus):
+       #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
+       key = params
+       if len(vstatus.cur_pin) < PIN_LENGTH:
+               if key == 11:
+                       if vstatus.cur_pin == '':
+                               vstatus.cur_user = ''
+                               vstatus.mk.set_message(GREETING)
+                       
+                               idle_in(vstatus,5)
+                               vstatus.change_state(STATE_IDLE)
 
-       def update_display(self, forced = False):
-               if not forced and self.next_update != None and time() < self.next_update:
+                               return
+                       vstatus.cur_pin = ''
+                       vstatus.mk.set_message('PIN: ')
                        return
-               if len(self.scrolling_message) > 0:
-                       if len(self.scrolling_message[0][0]) > 10:
-                               (m, r, t) = self.scrolling_message[0]
-                               a = []
-                               exp = HorizScroll(m).expand(padding = 0, wraparound = True)
-                               if t == None:
-                                       t = 0.1
-                               else:
-                                       t = t / len(exp)
-                               for x in exp:
-                                       a.append((x, r, t))
-                               del self.scrolling_message[0]
-                               self.scrolling_message = a + self.scrolling_message
-                       newmsg = self.scrolling_message[0]
-                       if newmsg[2] != None:
-                               self.next_update = time() + newmsg[2]
+               vstatus.cur_pin += chr(key + ord('0'))
+               vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
+               if len(vstatus.cur_pin) == PIN_LENGTH:
+                       vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
+                       if vstatus.username:
+                               v.beep(0, False)
+                               vstatus.cur_selection = ''
+                               vstatus.change_state(STATE_GET_SELECTION)
+                               scroll_options(vstatus.username, vstatus.mk, True)
+                               return
                        else:
-                               self.next_update = None
-                       self.v.display(self.scrolling_message[0][0])
-                       if self.scrolling_message[0][1]:
-                               self.scrolling_message.append(self.scrolling_message[0])
-                       del self.scrolling_message[0]
+                               v.beep(40, False)
+                               vstatus.mk.set_messages(
+                                       [(center('BAD PIN'), False, 1.0),
+                                        (center('SORRY'), False, 0.5),
+                                        (GREETING, False, None)])
+                               vstatus.cur_user = ''
+                               vstatus.cur_pin = ''
+                       
+                               idle_in(vstatus,5)
+                               vstatus.change_state(STATE_IDLE)
+
+                               return
+
+
+def handle_getting_uid_key(state, event, params, v, vstatus):
+       #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
+       key = params
+       # complicated key handling here:
+       if len(vstatus.cur_user) < 5:
+               if key == 11:
+                       vstatus.cur_user = ''
+                       vstatus.mk.set_message(GREETING)
+
+                       idle_in(vstatus,5)
+                       vstatus.change_state(STATE_IDLE)
+                       return
+
+               vstatus.cur_user += chr(key + ord('0'))
+               vstatus.mk.set_message('UID: '+vstatus.cur_user)
+
+       if len(vstatus.cur_user) == 5:
+               uid = int(vstatus.cur_user)
+               if not has_good_pin(uid):
+                       logging.info('user '+vstatus.cur_user+' has a bad PIN')
+                       vstatus.mk.set_messages(
+                               [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
+                                (GREETING, False, None)])
+                       vstatus.cur_user = ''
+                       vstatus.cur_pin = ''
+                       
+                       idle_in(vstatus,5)
+                       vstatus.change_state(STATE_IDLE)
+
+                       return
+
+
+               vstatus.cur_pin = ''
+               vstatus.mk.set_message('PIN: ')
+               logging.info('need pin for user %s'%vstatus.cur_user)
+               vstatus.change_state(STATE_GETTING_PIN)
+               return
+
+
+def handle_idle_key(state, event, params, v, vstatus):
+       #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
+
+       key = params
+
+       if key == 11:
+               vstatus.cur_user = ''
+               vstatus.mk.set_message(GREETING)
+               idle_in(vstatus,5)
+               choose_idler()
+               return
+       
+       vstatus.change_state(STATE_GETTING_UID)
+       run_handler(event, key, v, vstatus)
+
+
+def handle_idle_tick(state, event, params, v, vstatus):
+       ### State logged out ... initiate idler in 5  (first start?)
+       if vstatus.time_to_idle == None and vstatus.cur_user == '':
+               vstatus.time_to_idle = time() + 5
+               choose_idler()
+
+       ### State idling
+
+       if vstatus.time_to_idle is not None and time() > vstatus.time_to_idle: 
+               idle_step()
+
+       if vstatus.time_to_idle is not None and time() > vstatus.time_to_idle + 30:
+               vstatus.time_to_idle = time()
+               choose_idler()
+       
+       ###
+
+       vstatus.mk.update_display()
 
-       def done(self):
-               return len(self.scrolling_message) == 0
+       vstatus.change_state(STATE_GRANDFATHER_CLOCK)
+       run_handler(event, params, v, vstatus)
+       sleep(0.05)
+
+def beep_on(when, before=0):
+       start = int(when - before)
+       end = int(when)
+       now = int(time())
+
+       if now >= start and now <= end:
+               return 1
+       return 0
+
+def handle_idle_grandfather_tick(state, event, params, v, vstatus):
+       ### check for interesting times
+       now = localtime()
+
+       quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
+       halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
+       threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
+       fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
+
+       hourfromnow = localtime(time() + 3600)
+       
+       #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
+       onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
+               0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
+
+       ## check for X seconds to the hour
+       ## if case, update counter to 2
+       if beep_on(onthehour,15) \
+               or beep_on(halfhour,0) \
+               or beep_on(quarterhour,0) \
+               or beep_on(threequarterhour,0) \
+               or beep_on(fivetothehour,0):
+               vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
+               run_handler(event, params, v, vstatus)
+       else:
+               vstatus.change_state(STATE_IDLE)
+
+def handle_grandfather_tick(state, event, params, v, vstatus):
+       go_idle = 1
+
+       msg = []
+       ### we live in interesting times
+       now = localtime()
+
+       quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
+       halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
+       threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
+       fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
+
+       hourfromnow = localtime(time() + 3600)
+       
+#      onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
+       onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
+               0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
+
+
+       #print "when it fashionable to wear a onion on your hip"
+
+       if beep_on(onthehour,15):
+               go_idle = 0
+               next_hour=((hourfromnow[3] + 11) % 12) + 1
+               if onthehour - time() < next_hour and onthehour - time() > 0:
+                       v.beep(0, False)
+
+                       t = int(time())
+                       if (t % 2) == 0:
+                               msg.append(("DING!", False, None))
+                       else:
+                               msg.append(("     DING!", False, None))
+               elif int(onthehour - time()) == 0:
+                       v.beep(255, False)
+                       msg.append(("   BONG!", False, None))
+                       msg.append(("     IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
+       elif beep_on(halfhour,0):
+               go_idle = 0
+               v.beep(0, False)
+               msg.append((" HALFHOUR ", False, 50))
+       elif beep_on(quarterhour,0):
+               go_idle = 0
+               v.beep(0, False)
+               msg.append((" QTR HOUR ", False, 50))
+       elif beep_on(threequarterhour,0):
+               go_idle = 0
+               v.beep(0, False)
+               msg.append((" 3 QTR HR ", False, 50))
+       elif beep_on(fivetothehour,0):
+               go_idle = 0
+               v.beep(0, False)
+               msg.append(("Quick run to your lectures!  Hurry! Hurry!", False, TEXT_SPEED*4))
+       else:
+               go_idle = 1
+       
+       ## check for X seconds to the hour
+
+       if len(msg):
+               vstatus.mk.set_messages(msg)
+               sleep(1)
+
+       vstatus.mk.update_display()
+       ## if no longer case, return to idle
+
+       ## change idler to be clock
+       if go_idle and vstatus.mk.done():
+               vstatus.change_state(STATE_IDLE,1)
+
+def handle_door_idle(state, event, params, v, vstatus):
+       # don't care right now.
+       pass
+
+def handle_door_event(state, event, params, v, vstatus):
+       vstatus.time_to_idle = None
+
+       if params == 1:  #door open
+               vstatus.change_state(STATE_DOOR_OPENING)
+               logging.warning("Entering open door mode")
+               v.display("-FEED  ME-")
+               #door_open_mode(v);
+               vstatus.cur_user = ''
+               vstatus.cur_pin = ''
+       elif params == 0:  #door closed
+               vstatus.change_state(STATE_DOOR_CLOSING)
+               idle_in(vstatus, 5)
+
+               logging.warning('Leaving open door mode')
+               v.display("-YUM YUM!-")
+
+def idle_in(vstatus,seconds):
+       vstatus.time_to_idle = time() + seconds
+
+def return_to_idle(state,event,params,v,vstatus):
+       if vstatus.time_to_idle is not None and time() > vstatus.time_to_idle: 
+               vstatus.mk.set_message(GREETING)
+               vstatus.change_state(STATE_IDLE)
+               return
+       if not vstatus.time_to_idle:
+               vstatus.mk.set_message(GREETING)
+               vstatus.change_state(STATE_IDLE)
+               return
+
+def create_state_table(vstatus):
+       vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
+       vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
+       vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
+
+       vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
+       vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
+       vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
+
+       vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
+       vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
+       vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
+
+       vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
+       vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
+       vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
+
+       vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
+       vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
+       vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
+
+       vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
+       vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
+       vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
+
+       vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
+       vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
+       vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
+       vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
+       vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
+       vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
+
+def get_state_table_handler(vstatus, state, event, counter):
+       return vstatus.state_table[(state,event,counter)]
 
 def run_forever(rfh, wfh, options, cf):
        v = VendingMachine(rfh, wfh)
-       print 'PING is', v.ping()
+       vstatus = VendState(v)
+       create_state_table(vstatus)
+
+       logging.debug('PING is ' + str(v.ping()))
 
        if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
-       cur_user = ''
-       cur_pin = ''
-       cur_selection = ''
-
-       mk = MessageKeeper(v)
-       mk.set_message(GREETING)
-       time_to_autologout = None
-       #idler = TrainIdler(v)
-       #idler = GrayIdler(v)
-       idler = GrayIdler(v,one="*",zero="-")
-       time_to_idle = None
-       last_timeout_refresh = None
+
+       setup_idlers(v)
+       choose_idler()
+       vstatus.mk.set_message(GREETING)
+
+       # This main loop was hideous and the work of the devil.
+       # This has now been fixed (mostly) - mtearle
+       #
+       #
+       # notes for later surgery
+       #   (event, counter, ' ')
+       #        V
+       #   d[      ] = (method)
+       #
+       # ( return state - not currently implemented )
+
+       vstatus.change_state(STATE_IDLE,1)
 
        while True:
-               if USE_DB: db.handle_events()
-
-               if time_to_autologout != None:
-                       time_left = time_to_autologout - time()
-                       if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
-                               mk.set_message('LOGOUT: '+str(int(time_left)))
-                               last_timeout_refresh = int(time_left)
-                               cur_selection = ''
-
-               if time_to_autologout != None and time_to_autologout - time() <= 0:
-                       time_to_autologout = None
-                       cur_user = ''
-                       cur_pin = ''
-                       cur_selection = ''
-                       mk.set_message(GREETING)
-
-               if time_to_autologout and not mk.done(): time_to_autologout = None
-               if cur_user == '' and time_to_autologout: time_to_autologout = None
-               if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
-                       # start autologout
-                       time_to_autologout = time() + 15
-
-               if time_to_idle == None and cur_user == '': time_to_idle = time() + 60
-               if time_to_idle != None and cur_user != '': time_to_idle = None
-               if time_to_idle is not None and time() > time_to_idle: idler.next()
-
-               mk.update_display()
+               if USE_DB:
+                       try:
+                               db.handle_events()
+                       except DispenseDatabaseException, e:
+                               logging.error('Database error: '+str(e))
+
 
                e = v.next_event(0)
-               if e == None:
-                       e = v.next_event(0.1)
-                       if e == None:
-                               continue
-               time_to_idle = None
                (event, params) = e
-               print e
-               if event == DOOR:
-                       if params == 0:
-                               door_open_mode(v);
-                               cur_user = ''
-                               cur_pin = ''
-                               mk.set_message(GREETING)
-               elif event == SWITCH:
-                       # don't care right now.
-                       pass
-               elif event == KEY:
-                       key = params
-                       # complicated key handling here:
-                       if len(cur_user) < 5:
-                               if key == 11:
-                                       cur_user = ''
-                                       mk.set_message(GREETING)
-                                       continue
-                               cur_user += chr(key + ord('0'))
-                               mk.set_message('UID: '+cur_user)
-                               if len(cur_user) == 5:
-                                       uid = int(cur_user)
-                                       if not has_good_pin(uid):
-                                               #mk.set_messages(
-                                                       #[(center('INVALID'), False, 0.7),
-                                                        #(center('PIN'), False, 0.7),
-                                                        #(center('SETUP'), False, 1.0),
-                                                        #(GREETING, False, None)])
-                                               mk.set_messages(
-                                                       [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
-                                                        (GREETING, False, None)])
-                                               cur_user = ''
-                                               cur_pin = ''
-                                               continue
-                                       cur_pin = ''
-                                       mk.set_message('PIN: ')
-                                       continue
-                       elif len(cur_pin) < PIN_LENGTH:
-                               if key == 11:
-                                       if cur_pin == '':
-                                               cur_user = ''
-                                               mk.set_message(GREETING)
-                                               continue
-                                       cur_pin = ''
-                                       mk.set_message('PIN: ')
-                                       continue
-                               cur_pin += chr(key + ord('0'))
-                               mk.set_message('PIN: '+'X'*len(cur_pin))
-                               if len(cur_pin) == PIN_LENGTH:
-                                       username = verify_user_pin(int(cur_user), int(cur_pin))
-                                       if username:
-                                               v.beep(0, False)
-                                               cur_selection = ''
-                                               scroll_options(username, mk, True)
-                                               continue
-                                       else:
-                                               v.beep(40, False)
-                                               mk.set_messages(
-                                                       [(center('BAD PIN'), False, 1.0),
-                                                        (center('SORRY'), False, 0.5),
-                                                        (GREETING, False, None)])
-                                               cur_user = ''
-                                               cur_pin = ''
-                                               continue
-                       elif len(cur_selection) == 0:
-                               if key == 11:
-                                       cur_pin = ''
-                                       cur_user = ''
-                                       cur_selection = ''
-                                       mk.set_messages(
-                                               [(center('BYE!'), False, 1.5),
-                                                (GREETING, False, None)])
-                                       continue
-                               cur_selection += chr(key + ord('0'))
-                               mk.set_message('SELECT: '+cur_selection)
-                               time_to_autologout = None
-                       elif len(cur_selection) == 1:
-                               if key == 11:
-                                       cur_selection = ''
-                                       time_to_autologout = None
-                                       scroll_options(username, mk)
-                                       continue
-                               else:
-                                       cur_selection += chr(key + ord('0'))
-                                       #make_selection(cur_selection)
-                                       # XXX this should move somewhere else:
-                                       if cur_selection == '55':
-                                               mk.set_message('OPENSESAME')
-                                               ret = os.system('su - "%s" -c "dispense door"'%username)
-                                               if ret == 0:
-                                                       mk.set_message(center('DOOR OPEN'))
-                                               else:
-                                                       mk.set_message(center('BAD DOOR'))
-                                               sleep(1)
-                                       elif cur_selection == '91':
-                                               cookie(v)
-                                       elif cur_selection == '99':
-                                               scroll_options(username, mk)
-                                               cur_selection = ''
-                                               continue
-                                       elif cur_selection[1] == '8':
-                                               v.display('GOT COKE?')
-                                               os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
-                                       else:
-                                               v.display('HERES A '+cur_selection)
-                                               v.vend(cur_selection)
-                                       sleep(0.5)
-                                       v.display('THANK YOU')
-                                       sleep(0.5)
-                                       cur_selection = ''
-                                       time_to_autologout = time() + 8
+
+               run_handler(event, params, v, vstatus)
+
+#              logging.debug('Got event: ' + repr(e))
+
+
+def run_handler(event, params, v, vstatus):
+       handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
+       if handler:
+               handler(vstatus.state, event, params, v, vstatus)
 
 def connect_to_vend(options, cf):
-       # Open vending machine via LAT?
+
        if options.use_lat:
+               logging.info('Connecting to vending machine using LAT')
                latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
                rfh, wfh = latclient.get_fh()
+       elif options.use_serial:
+               # Open vending machine via serial.
+               logging.info('Connecting to vending machine using serial')
+               serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
+               rfh,wfh = serialclient.get_fh()
        else:
                #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
+               logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
                import socket
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
                sock.connect((options.host, options.port))
@@ -384,9 +775,17 @@ def parse_args():
 
        op = OptionParser(usage="%prog [OPTION]...")
        op.add_option('-f', '--config-file', default='/etc/dispense/servers.conf', metavar='FILE', dest='config_file', help='use the specified config file instead of /etc/dispense/servers.conf')
-       op.add_option('-v', '--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
+       op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
+       op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
+       op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
        op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
        op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
+       op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
+       op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
+       op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
+       op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
+       op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
+       op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
        options, args = op.parse_args()
 
        if len(args) != 0:
@@ -420,49 +819,126 @@ class VendConfigFile:
                                self.__dict__[option] = value
                
                except ConfigParser.Error, e:
-                       print "Error reading config file "+config_file+": " + str(e)
-                       sys.exit(1)
+                       raise SystemExit("Error reading config file "+config_file+": " + str(e))
+
+def create_pid_file(name):
+       try:
+               pid_file = file(name, 'w')
+               pid_file.write('%d\n'%os.getpid())
+               pid_file.close()
+       except IOError, e:
+               logging.warning('unable to write to pid file '+name+': '+str(e))
+
+def set_stuff_up():
+       def do_nothing(signum, stack):
+               signal.signal(signum, do_nothing)
+       def stop_server(signum, stack): raise KeyboardInterrupt
+       signal.signal(signal.SIGHUP, do_nothing)
+       signal.signal(signal.SIGTERM, stop_server)
+       signal.signal(signal.SIGINT, stop_server)
 
-def do_vend_server():
        options = parse_args()
        config_opts = VendConfigFile(options.config_file, config_options)
+       if options.daemon: become_daemon()
+       set_up_logging(options)
+       if options.pid_file != '': create_pid_file(options.pid_file)
+
+       return options, config_opts
+
+def clean_up_nicely(options, config_opts):
+       if options.pid_file != '':
+               try:
+                       os.unlink(options.pid_file)
+                       logging.debug('Removed pid file '+options.pid_file)
+               except OSError: pass  # if we can't delete it, meh
 
+def set_up_logging(options):
+       logger = logging.getLogger()
+       
+       if not options.daemon:
+               stderr_logger = logging.StreamHandler(sys.stderr)
+               stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
+               logger.addHandler(stderr_logger)
+       
+       if options.log_file != '':
+               try:
+                       file_logger = logging.FileHandler(options.log_file)
+                       file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
+                       logger.addHandler(file_logger)
+               except IOError, e:
+                       logger.warning('unable to write to log file '+options.log_file+': '+str(e))
+
+       if options.syslog != None:
+               sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
+               sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
+               logger.addHandler(sys_logger)
+
+       if options.quiet:
+               logger.setLevel(logging.WARNING)
+       elif options.verbose:
+               logger.setLevel(logging.DEBUG)
+       else:
+               logger.setLevel(logging.INFO)
+
+def become_daemon():
+       dev_null = file('/dev/null')
+       fd = dev_null.fileno()
+       os.dup2(fd, 0)
+       os.dup2(fd, 1)
+       os.dup2(fd, 2)
+       try:
+               if os.fork() != 0:
+                       sys.exit(0)
+               os.setsid()
+       except OSError, e:
+               raise SystemExit('failed to fork: '+str(e))
+
+def do_vend_server(options, config_opts):
        while True:
                try:
                        rfh, wfh = connect_to_vend(options, config_opts)
-               except (LATClientException, socket.error), e:
+               except (SerialClientException, socket.error), e:
                        (exc_type, exc_value, exc_traceback) = sys.exc_info()
                        del exc_traceback
-                       print
-                       print "Connection error: "+str(exc_type)+" "+str(e)
-                       print "Trying again in 5 seconds."
+                       logging.error("Connection error: "+str(exc_type)+" "+str(e))
+                       logging.info("Trying again in 5 seconds.")
                        sleep(5)
                        continue
+               
                try:
                        run_forever(rfh, wfh, options, config_opts)
                except VendingException:
-                       print
-                       print "Connection died, trying again..."
-
+                       logging.error("Connection died, trying again...")
+                       logging.info("Trying again in 5 seconds.")
+                       sleep(5)
 
 if __name__ == '__main__':
+       options, config_opts = set_stuff_up()
        while True:
                try:
-                       do_vend_server()
+                       logging.warning('Starting Vend Server')
+                       do_vend_server(options, config_opts)
+                       logging.error('Vend Server finished unexpectedly, restarting')
                except KeyboardInterrupt:
-                       print "Killed by SIGINT."
+                       logging.info("Killed by signal, cleaning up")
+                       clean_up_nicely(options, config_opts)
+                       logging.warning("Vend Server stopped")
+                       break
+               except SystemExit:
                        break
                except:
                        (exc_type, exc_value, exc_traceback) = sys.exc_info()
-                       print
-                       print "Uh-oh, unhandled " + str(exc_type) + " exception"
-                       print "Message: ", str(exc_value)
-                       print
-                       print_tb(exc_traceback)
+                       tb = format_tb(exc_traceback, 20)
                        del exc_traceback
-                       print
-                       print "This message should be considered a bug in the Vend Server."
-                       print "Please report this to someone who can fix it."
-                       print
-                       print "Trying again anyway (might not help, but hey...)"
+                       
+                       logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
+                       logging.critical("Message: " + str(exc_value))
+                       logging.critical("Traceback:")
+                       for event in tb:
+                               for line in event.split('\n'):
+                                       logging.critical('    '+line)
+                       logging.critical("This message should be considered a bug in the Vend Server.")
+                       logging.critical("Please report this to someone who can fix it.")
+                       sleep(10)
+                       logging.warning("Trying again anyway (might not help, but hey...)")
 

UCC git Repository :: git.ucc.asn.au