# vim:ts=4
USE_DB = 0
+USE_MIFARE = 1
-import sys, os, string, re, pwd
+import ConfigParser
+import sys, os, string, re, pwd, signal, math, syslog
+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
-from VendingMachine import VendingMachine
-from ConfigParser import ConfigParser
+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 GreetingIdler,TrainIdler,GrayIdler,StringIdler,ClockIdler,FortuneIdler,FileIdler,PipeIdler
+from SnackConfig import get_snacks, get_snack
+import socket
+from posix import geteuid
+from LDAPConnector import get_uid, set_card_id
+
+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
DOOR = 1
SWITCH = 2
KEY = 3
+TICK = 4
+MIFARE = 5
+
+
+(
+STATE_IDLE,
+STATE_DOOR_OPENING,
+STATE_DOOR_CLOSING,
+STATE_GETTING_UID,
+STATE_GETTING_PIN,
+STATE_GET_SELECTION,
+STATE_GRANDFATHER_CLOCK,
+) = range(1,8)
+
+TEXT_SPEED = 0.8
+IDLE_SPEED = 0.05
+
+class DispenseDatabaseException(Exception): pass
class DispenseDatabase:
def __init__(self, vending_machine, host, name, user, password):
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()
def scroll_options(username, mk, welcome = False):
if welcome:
- msg = [(center('WELCOME'), False, 0.8),
- (center(username), False, 0.8)]
+ # Balance checking: crap code, [DAA]'s fault
+ acct = os.popen('dispense acct %s' % username)
+ balance = acct.read()[15:22]
+ acct.close()
+
+ msg = [(center('WELCOME'), False, TEXT_SPEED),
+ (center(username), False, TEXT_SPEED),
+ (center(balance), False, TEXT_SPEED),]
else:
msg = []
choices = ' '*10+'CHOICES: '
c = c.strip()
(slot_num, price, slot_name) = c.split(' ', 2)
if slot_name == 'dead': continue
- choices += '%s8-%s (%sc) '%(slot_num, slot_name, price)
+ choices += '%s-(%sc)-%s8 '%(slot_name, price, slot_num)
+
+# we don't want to print snacks for now since it'll be too large
+# and there's physical bits of paper in the machine anyway - matt
+# try:
+# snacks = get_snacks()
+# except:
+# snacks = {}
+#
+# for slot, ( name, price ) in snacks.items():
+# choices += '%s8-%s (%sc) ' % ( slot, name, price )
+
choices += '55-DOOR '
- choices += 'OR A SNACK. '
+ choices += 'OR ANOTHER SNACK. '
choices += '99 TO READ AGAIN. '
choices += 'CHOICE? '
msg.append((choices, False, None))
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)
def has_good_pin(uid):
return get_pin(uid) != None
-def verify_user_pin(uid, pin):
- if get_pin(uid) == pin:
+def verify_user_pin(uid, pin, skip_pin_check=False):
+ if skip_pin_check or get_pin(uid) == pin:
info = pwd.getpwuid(uid)
+ if skip_pin_check:
+ logging.info('accepted mifare for uid %d (%s)'%(uid,info.pw_name))
+ else:
+ 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(vending_machine):
- print "Entering open door mode"
- v.display("DOOR OPEN")
- while True:
- e = v.next_event()
- if e == None: break
- (event, params) = e
- if event == DOOR:
- if params == 1: # door closed
- v.display("BYE BYE!")
- sleep(1)
- return
+
+def cookie(v):
+ seed(time())
+ messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
+ choice = int(random()*len(messages))
+ msg = messages[choice]
+ left = range(len(msg))
+ for i in range(len(msg)):
+ if msg[i] == ' ': left.remove(i)
+ reveal = 1
+ while left:
+ s = ''
+ for i in range(0, len(msg)):
+ if i in left:
+ if reveal == 0:
+ left.remove(i)
+ s += msg[i]
+ else:
+ s += chr(int(random()*26)+ord('A'))
+ reveal += 1
+ reveal %= 17
+ else:
+ s += msg[i]
+ v.display(s)
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',affinity=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 [now closed? - MSH] - and order as Quinn - I am getting really hungry", repeat=False),
+ PipeIdler(v, "/usr/bin/getent", "passwd"),
+ FortuneIdler(v),
+ ClockIdler(v),
+ StringIdler(v),
+ TrainIdler(v),
+ ]
+ disabled = [
+ ]
+
+def reset_idler(v, vstatus, t = None):
+ global idlers, idler
+ idler = GreetingIdler(v, t)
+ vstatus.time_of_next_idlestep = time()+idler.next()
+ vstatus.time_of_next_idler = None
+ vstatus.time_to_autologout = None
+ vstatus.change_state(STATE_IDLE, 1)
+
+def choose_idler():
+ global idlers, idler
+ iiindex = 0
+ average_affinity = 10 # guessing here...
+
+ if idler and idler.__class__ != GreetingIdler:
+ iiindex = idlers.index(idler)
- def update_display(self, forced = False):
- if not forced and self.next_update != None and time() < self.next_update:
+ iilen = len(idlers)
+
+ move = int(random()*len(idlers)*average_affinity) + 1
+
+ while move >= 0:
+ iiindex += 1
+ iiindex %= iilen
+ idler = idlers[iiindex]
+ move -= idler.affinity()
+
+ idler.reset()
+
+def idle_step(vstatus):
+ global idler
+ if idler.finished():
+ choose_idler()
+ vstatus.time_of_next_idler = time() + 30
+ nextidle = idler.next()
+ if nextidle is None:
+ nextidle = IDLE_SPEED
+ vstatus.time_of_next_idlestep = time()+nextidle
+
+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.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 = ''
+
+ reset_idler(v, vstatus)
+
+ ### 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
+
+ ## 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 = ''
+
+ vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
+ reset_idler(v, vstatus, 2)
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 = False)
- 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_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'))
+ if vstatus.cur_user:
+ make_selection(v,vstatus)
+ vstatus.cur_selection = ''
+ vstatus.time_to_autologout = time() + 8
+ vstatus.last_timeout_refresh = None
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]
+ # Price check mode.
+ price_check(v,vstatus)
+ vstatus.cur_selection = ''
+ vstatus.time_to_autologout = None
+ vstatus.last_timeout_refresh = None
- def done(self):
- return len(self.scrolling_message) == 0
+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 == '81':
+ 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 DRINK?')
+ if ((os.system('su - "%s" -c "dispense %s"'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
+ v.display('SEEMS NOT')
+ else:
+ v.display('GOT DRINK!')
+ else:
+ # first see if it's a named slot
+ try:
+ price, shortname, name = get_snack( vstatus.cur_selection )
+ except:
+ price, shortname, name = get_snack( '--' )
+ dollarprice = "$%.2f" % ( price / 100.0 )
+ v.display(vstatus.cur_selection+' - %s'%dollarprice)
+ exitcode = os.system('su - "%s" -c "dispense give oday %d"'%(vstatus.username, price)) >> 8
+ if (exitcode == 0):
+ # magic dispense syslog service
+ syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
+ v.vend(vstatus.cur_selection)
+ v.display('THANK YOU')
+ else:
+ syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
+ v.display('NO MONEY?')
+ sleep(1)
-if __name__ == '__main__':
- cp = ConfigParser()
- cp.read('/etc/dispense/servers.conf')
- DBServer = cp.get('Database', 'Server')
- DBName = cp.get('Database', 'Name')
- DBUser = cp.get('VendingMachine', 'DBUser')
- DBPassword = cp.get('VendingMachine', 'DBPassword')
-
- ServiceName = cp.get('VendingMachine', 'ServiceName')
- ServicePassword = cp.get('VendingMachine', 'Password')
- # Open vending machine via LAT
- if 0:
- latclient = LATClient(service = ServiceName, password = ServicePassword)
- (rfh, wfh) = latclient.get_fh()
+
+def price_check(v, vstatus):
+ if vstatus.cur_selection[1] == '8':
+ v.display(center('SEE COKE'))
+ else:
+ # first see if it's a named slot
+ try:
+ price, shortname, name = get_snack( vstatus.cur_selection )
+ except:
+ price, shortname, name = get_snack( '--' )
+ dollarprice = "$%.2f" % ( price / 100.0 )
+ v.display(vstatus.cur_selection+' - %s'%dollarprice)
+
+
+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 = ''
+ reset_idler(v, vstatus)
+
+ return
+ vstatus.cur_pin = ''
+ vstatus.mk.set_message('PIN: ')
+ return
+ 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:
+ v.beep(40, False)
+ vstatus.mk.set_messages(
+ [(center('BAD PIN'), False, 1.0),
+ (center('SORRY'), False, 0.5)])
+ vstatus.cur_user = ''
+ vstatus.cur_pin = ''
+
+ reset_idler(v, vstatus, 2)
+
+ 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) == 0 and key == 9:
+ vstatus.cur_selection = ''
+ vstatus.time_to_autologout = None
+ vstatus.mk.set_message('PRICECHECK')
+ sleep(0.5)
+ scroll_options('', vstatus.mk)
+ vstatus.change_state(STATE_GET_SELECTION)
+ return
+
+ if len(vstatus.cur_user) <8:
+ if key == 11:
+ vstatus.cur_user = ''
+
+ reset_idler(v, vstatus)
+ return
+ vstatus.cur_user += chr(key + ord('0'))
+ #logging.info('dob: '+vstatus.cur_user)
+ if len(vstatus.cur_user) > 5:
+ vstatus.mk.set_message('>'+vstatus.cur_user)
+ else:
+ vstatus.mk.set_message('UID: '+vstatus.cur_user)
+
+ if len(vstatus.cur_user) == 5:
+ uid = int(vstatus.cur_user)
+
+ if uid == 0:
+ logging.info('user '+vstatus.cur_user+' has a bad PIN')
+ pfalken="""
+CARRIER DETECTED
+
+CONNECT 128000
+
+Welcome to Picklevision Sytems, Sunnyvale, CA
+
+Greetings Professor Falken.
+
+
+
+
+Shall we play a game?
+
+
+Please choose from the following menu:
+
+1. Tic-Tac-Toe
+2. Chess
+3. Checkers
+4. Backgammon
+5. Poker
+6. Toxic and Biochemical Warfare
+7. Global Thermonuclear War
+
+7 [ENTER]
+
+Wouldn't you prefer a nice game of chess?
+
+""".replace('\n',' ')
+ vstatus.mk.set_messages([(pfalken, False, 10)])
+ vstatus.cur_user = ''
+ vstatus.cur_pin = ''
+
+ reset_idler(v, vstatus, 10)
+
+ return
+
+ if not has_good_pin(uid):
+ logging.info('user '+vstatus.cur_user+' has a bad PIN')
+ vstatus.mk.set_messages(
+ [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
+ vstatus.cur_user = ''
+ vstatus.cur_pin = ''
+
+ reset_idler(v, vstatus, 3)
+
+ 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 = ''
+ reset_idler(v, vstatus)
+ return
+
+ vstatus.change_state(STATE_GETTING_UID)
+ run_handler(event, key, v, vstatus)
+
+
+def handle_idle_tick(state, event, params, v, vstatus):
+ ### State idling
+ if vstatus.mk.done():
+ idle_step(vstatus)
+
+ if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
+ vstatus.time_of_next_idler = time() + 30
+ choose_idler()
+
+ ###
+
+ vstatus.mk.update_display()
+
+ 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):
+ def twiddle(clock,v,wise = 2):
+ if (clock % 4 == 0):
+ v.display("-FEED ME-")
+ elif (clock % 4 == 1+wise):
+ v.display("\\FEED ME/")
+ elif (clock % 4 == 2):
+ v.display("-FEED ME-")
+ elif (clock % 4 == 3-wise):
+ v.display("/FEED ME\\")
+
+ # don't care right now.
+ now = int(time())
+
+ if ((now % 60 % 2) == 0):
+ twiddle(now, v)
+ else:
+ twiddle(now, v, wise=0)
+
+
+def handle_door_event(state, event, params, v, vstatus):
+ if params == 0: #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 == 1: #door closed
+ vstatus.change_state(STATE_DOOR_CLOSING)
+ reset_idler(v, vstatus, 3)
+
+ logging.warning('Leaving open door mode')
+ v.display("-YUM YUM!-")
+
+def handle_mifare_event(state, event, params, v, vstatus):
+ card_id = params
+ # Translate card_id into uid.
+ if card_id == None:
+ return
+
+ try:
+ vstatus.cur_user = get_uid(card_id)
+ logging.info('Mapped card id to uid %s'%vstatus.cur_user)
+ vstatus.username = verify_user_pin(int(vstatus.cur_user), None, True)
+ except ValueError:
+ vstatus.username = None
+ 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:
+ v.beep(40, False)
+ vstatus.mk.set_messages(
+ [(center('BAD CARD'), False, 1.0),
+ (center('SORRY'), False, 0.5)])
+ vstatus.cur_user = ''
+ vstatus.cur_pin = ''
+
+ reset_idler(v, vstatus, 2)
+ return
+
+def handle_mifare_add_user_event(state, event, params, v, vstatus):
+ card_id = params
+
+ # Translate card_id into uid.
+ if card_id == None:
+ return
+
+ try:
+ if get_uid(card_id) != None:
+ vstatus.mk.set_messages(
+ [(center('ALREADY'), False, 0.5),
+ (center('ENROLLED'), False, 0.5)])
+
+ # scroll_options(vstatus.username, vstatus.mk)
+ return
+ except ValueError:
+ pass
+
+ logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
+ set_card_id(vstatus.cur_user, card_id)
+ vstatus.mk.set_messages(
+ [(center('CARD'), False, 0.5),
+ (center('ENROLLED'), False, 0.5)])
+
+ # scroll_options(vstatus.username, vstatus.mk)
+
+def return_to_idle(state,event,params,v,vstatus):
+ reset_idler(v, vstatus)
+
+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_IDLE,MIFARE,1)] = handle_mifare_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_OPENING,MIFARE,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_DOOR_CLOSING,MIFARE,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_UID,MIFARE,1)] = handle_mifare_event
+
+ 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_GETTING_PIN,MIFARE,1)] = handle_mifare_event
+
+ 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_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
+
+ 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
+ vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
+
+def get_state_table_handler(vstatus, state, event, counter):
+ return vstatus.state_table[(state,event,counter)]
+
+def time_to_next_update(vstatus):
+ idle_update = vstatus.time_of_next_idlestep - time()
+ if not vstatus.mk.done() and vstatus.mk.next_update is not None:
+ mk_update = vstatus.mk.next_update - time()
+ if mk_update < idle_update:
+ idle_update = mk_update
+ return idle_update
+
+def run_forever(rfh, wfh, options, cf):
+ v = VendingMachine(rfh, wfh, USE_MIFARE)
+ 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)
+
+ setup_idlers(v)
+ reset_idler(v, vstatus)
+
+ # 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 )
+
+ while True:
+ if USE_DB:
+ try:
+ db.handle_events()
+ except DispenseDatabaseException, e:
+ logging.error('Database error: '+str(e))
+
+ timeout = time_to_next_update(vstatus)
+ e = v.next_event(timeout)
+ (event, params) = e
+
+ 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):
+
+ 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(('localhost', 5150))
+ sock.connect((options.host, options.port))
rfh = sock.makefile('r')
wfh = sock.makefile('w')
- v = VendingMachine(rfh, wfh)
- print 'PING is', v.ping()
+ global USE_MIFARE
+ USE_MIFARE = 0
+
+ return rfh, wfh
+
+def parse_args():
+ from optparse import OptionParser
+
+ 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('--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 USE_DB: db = DispenseDatabase(v, DBServer, DBName, DBUser, DBPassword)
- cur_user = ''
- cur_pin = ''
- cur_selection = ''
+ if len(args) != 0:
+ op.error('extra command line arguments: ' + ' '.join(args))
- mk = MessageKeeper(v)
- mk.set_message(GREETING)
- logout_timeout = None
- last_timeout_refresh = None
+ return options
+
+config_options = {
+ 'DBServer': ('Database', 'Server'),
+ 'DBName': ('Database', 'Name'),
+ 'DBUser': ('VendingMachine', 'DBUser'),
+ 'DBPassword': ('VendingMachine', 'DBPassword'),
+
+ 'ServiceName': ('VendingMachine', 'ServiceName'),
+ 'ServicePassword': ('VendingMachine', 'Password'),
+
+ 'ServerName': ('DecServer', 'Name'),
+ 'ConnectPassword': ('DecServer', 'ConnectPassword'),
+ 'PrivPassword': ('DecServer', 'PrivPassword'),
+ }
+
+class VendConfigFile:
+ def __init__(self, config_file, options):
+ try:
+ cp = ConfigParser.ConfigParser()
+ cp.read(config_file)
+ for option in options:
+ section, name = options[option]
+ value = cp.get(section, name)
+ self.__dict__[option] = value
+
+ except ConfigParser.Error, e:
+ 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)
+
+ 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:
- if USE_DB: db.handle_events()
-
- if logout_timeout != None:
- time_left = logout_timeout - time()
- if time_left < 5 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 logout_timeout != None and logout_timeout - time() <= 0:
- logout_timeout = None
- cur_user = ''
- cur_pin = ''
- cur_selection = ''
- mk.set_message(GREETING)
-
- if logout_timeout and not mk.done(): logout_timeout = None
- if len(cur_pin) == PIN_LENGTH and mk.done() and logout_timeout == None:
- # start autologout
- logout_timeout = time() + 15
-
- mk.update_display()
-
- e = v.next_event(0)
- if e == None:
- e = v.next_event(0.1)
- if e == None:
- continue
- (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)])
- 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 = ''
- v.display('BYE!')
- sleep(0.5)
- mk.set_message(GREETING)
- continue
- cur_selection += chr(key + ord('0'))
- mk.set_message('SELECT: '+cur_selection)
- elif len(cur_selection) == 1:
- if key == 11:
- cur_selection = ''
- logout_timeout = 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':
- v.display('GOT DOOR?')
- os.system('su - "%s" -c "dispense door"'%username)
- 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 = ''
- logout_timeout = time() + 10
+ try:
+ rfh, wfh = connect_to_vend(options, config_opts)
+ except (SerialClientException, socket.error), e:
+ (exc_type, exc_value, exc_traceback) = sys.exc_info()
+ del exc_traceback
+ 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:
+ 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:
+ logging.warning('Starting Vend Server')
+ do_vend_server(options, config_opts)
+ logging.error('Vend Server finished unexpectedly, restarting')
+ except KeyboardInterrupt:
+ 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()
+ tb = format_tb(exc_traceback, 20)
+ del exc_traceback
+
+ 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...)")
+