7 import sys, os, string, re, pwd, signal
8 from traceback import print_tb
10 from time import time, sleep
11 from popen2 import popen2
12 from LATClient import LATClient, LATClientException
13 from VendingMachine import VendingMachine, VendingException
14 from HorizScroll import HorizScroll
15 from random import random, seed
16 from Idler import TrainIdler,GrayIdler
18 from posix import geteuid
20 GREETING = 'UCC SNACKS'
27 class DispenseDatabase:
28 def __init__(self, vending_machine, host, name, user, password):
29 self.vending_machine = vending_machine
30 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
31 self.db.query('LISTEN vend_requests')
33 def process_requests(self):
35 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
37 outstanding = self.db.query(query).getresult()
38 except (pg.error,), db_err:
39 sys.stderr.write('Failed to query database: %s\n'%(db_err.strip()))
41 for (id, slot) in outstanding:
42 (worked, code, string) = self.vending_machine.vend(slot)
43 print (worked, code, string)
45 query = 'SELECT vend_success(%s)'%id
46 self.db.query(query).getresult()
48 query = 'SELECT vend_failed(%s)'%id
49 self.db.query(query).getresult()
51 def handle_events(self):
52 notifier = self.db.getnotify()
53 while notifier is not None:
54 self.process_requests()
55 notify = self.db.getnotify()
57 def scroll_options(username, mk, welcome = False):
59 msg = [(center('WELCOME'), False, 0.8),
60 (center(username), False, 0.8)]
63 choices = ' '*10+'CHOICES: '
65 coke_machine = file('/home/other/coke/coke_contents')
66 cokes = coke_machine.readlines()
73 (slot_num, price, slot_name) = c.split(' ', 2)
74 if slot_name == 'dead': continue
75 choices += '%s8-%s (%sc) '%(slot_num, slot_name, price)
77 choices += 'OR A SNACK. '
78 choices += '99 TO READ AGAIN. '
80 msg.append((choices, False, None))
85 info = pwd.getpwuid(uid)
88 if info.pw_dir == None: return False
89 pinfile = os.path.join(info.pw_dir, '.pin')
100 pinstr = f.readline()
102 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
106 def has_good_pin(uid):
107 return get_pin(uid) != None
109 def verify_user_pin(uid, pin):
110 if get_pin(uid) == pin:
111 info = pwd.getpwuid(uid)
116 def door_open_mode(v):
117 print "Entering open door mode"
118 v.display("-FEED ME-")
124 if params == 1: # door closed
125 v.display("-YUM YUM!-")
131 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
132 choice = int(random()*len(messages))
133 msg = messages[choice]
134 left = range(len(msg))
135 for i in range(len(msg)):
136 if msg[i] == ' ': left.remove(i)
140 for i in range(0, len(msg)):
146 s += chr(int(random()*26)+ord('A'))
155 return ' '*((LEN-len(str))/2)+str
158 def __init__(self, vendie):
159 # Each element of scrolling_message should be a 3-tuple of
160 # ('message', True/False if it is to be repeated, time to display)
161 self.scrolling_message = []
163 self.next_update = None
165 def set_message(self, string):
166 self.scrolling_message = [(string, False, None)]
167 self.update_display(True)
169 def set_messages(self, strings):
170 self.scrolling_message = strings
171 self.update_display(True)
173 def update_display(self, forced = False):
174 if not forced and self.next_update != None and time() < self.next_update:
176 if len(self.scrolling_message) > 0:
177 if len(self.scrolling_message[0][0]) > 10:
178 (m, r, t) = self.scrolling_message[0]
180 exp = HorizScroll(m).expand(padding = 0, wraparound = True)
187 del self.scrolling_message[0]
188 self.scrolling_message = a + self.scrolling_message
189 newmsg = self.scrolling_message[0]
190 if newmsg[2] != None:
191 self.next_update = time() + newmsg[2]
193 self.next_update = None
194 self.v.display(self.scrolling_message[0][0])
195 if self.scrolling_message[0][1]:
196 self.scrolling_message.append(self.scrolling_message[0])
197 del self.scrolling_message[0]
200 return len(self.scrolling_message) == 0
202 def run_forever(rfh, wfh, options, cf):
203 v = VendingMachine(rfh, wfh)
204 print 'PING is', v.ping()
206 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
211 mk = MessageKeeper(v)
212 mk.set_message(GREETING)
213 time_to_autologout = None
214 #idler = TrainIdler(v)
215 #idler = GrayIdler(v)
216 idler = GrayIdler(v,one="*",zero="-")
218 last_timeout_refresh = None
221 if USE_DB: db.handle_events()
223 if time_to_autologout != None:
224 time_left = time_to_autologout - time()
225 if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
226 mk.set_message('LOGOUT: '+str(int(time_left)))
227 last_timeout_refresh = int(time_left)
230 if time_to_autologout != None and time_to_autologout - time() <= 0:
231 time_to_autologout = None
235 mk.set_message(GREETING)
237 if time_to_autologout and not mk.done(): time_to_autologout = None
238 if cur_user == '' and time_to_autologout: time_to_autologout = None
239 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
241 time_to_autologout = time() + 15
243 if time_to_idle == None and cur_user == '': time_to_idle = time() + 60
244 if time_to_idle != None and cur_user != '': time_to_idle = None
245 if time_to_idle is not None and time() > time_to_idle: idler.next()
251 e = v.next_event(0.1)
262 mk.set_message(GREETING)
263 elif event == SWITCH:
264 # don't care right now.
268 # complicated key handling here:
269 if len(cur_user) < 5:
272 mk.set_message(GREETING)
274 cur_user += chr(key + ord('0'))
275 mk.set_message('UID: '+cur_user)
276 if len(cur_user) == 5:
278 if not has_good_pin(uid):
280 #[(center('INVALID'), False, 0.7),
281 #(center('PIN'), False, 0.7),
282 #(center('SETUP'), False, 1.0),
283 #(GREETING, False, None)])
285 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
286 (GREETING, False, None)])
291 mk.set_message('PIN: ')
293 elif len(cur_pin) < PIN_LENGTH:
297 mk.set_message(GREETING)
300 mk.set_message('PIN: ')
302 cur_pin += chr(key + ord('0'))
303 mk.set_message('PIN: '+'X'*len(cur_pin))
304 if len(cur_pin) == PIN_LENGTH:
305 username = verify_user_pin(int(cur_user), int(cur_pin))
309 scroll_options(username, mk, True)
314 [(center('BAD PIN'), False, 1.0),
315 (center('SORRY'), False, 0.5),
316 (GREETING, False, None)])
320 elif len(cur_selection) == 0:
326 [(center('BYE!'), False, 1.5),
327 (GREETING, False, None)])
329 cur_selection += chr(key + ord('0'))
330 mk.set_message('SELECT: '+cur_selection)
331 time_to_autologout = None
332 elif len(cur_selection) == 1:
335 time_to_autologout = None
336 scroll_options(username, mk)
339 cur_selection += chr(key + ord('0'))
340 #make_selection(cur_selection)
341 # XXX this should move somewhere else:
342 if cur_selection == '55':
343 mk.set_message('OPENSESAME')
345 ret = os.system('su - "%s" -c "dispense door"'%username)
347 ret = os.system('dispense door')
349 mk.set_message(center('DOOR OPEN'))
351 mk.set_message(center('BAD DOOR'))
353 elif cur_selection == '91':
355 elif cur_selection == '99':
356 scroll_options(username, mk)
359 elif cur_selection[1] == '8':
360 v.display('GOT COKE?')
361 os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
363 v.display('HERES A '+cur_selection)
364 v.vend(cur_selection)
366 v.display('THANK YOU')
369 time_to_autologout = time() + 8
371 def connect_to_vend(options, cf):
372 # Open vending machine via LAT?
374 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
375 rfh, wfh = latclient.get_fh()
377 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
379 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
380 sock.connect((options.host, options.port))
381 rfh = sock.makefile('r')
382 wfh = sock.makefile('w')
387 from optparse import OptionParser
389 op = OptionParser(usage="%prog [OPTION]...")
390 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')
391 op.add_option('-v', '--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
392 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
393 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
394 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
395 op.add_option('-s', '--syslog', dest='syslog', action='store_true', default=False, help='log output to syslog')
396 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
397 options, args = op.parse_args()
400 op.error('extra command line arguments: ' + ' '.join(args))
405 'DBServer': ('Database', 'Server'),
406 'DBName': ('Database', 'Name'),
407 'DBUser': ('VendingMachine', 'DBUser'),
408 'DBPassword': ('VendingMachine', 'DBPassword'),
410 'ServiceName': ('VendingMachine', 'ServiceName'),
411 'ServicePassword': ('VendingMachine', 'Password'),
413 'ServerName': ('DecServer', 'Name'),
414 'ConnectPassword': ('DecServer', 'ConnectPassword'),
415 'PrivPassword': ('DecServer', 'PrivPassword'),
418 class VendConfigFile:
419 def __init__(self, config_file, options):
421 cp = ConfigParser.ConfigParser()
424 for option in options:
425 section, name = options[option]
426 value = cp.get(section, name)
427 self.__dict__[option] = value
429 except ConfigParser.Error, e:
430 print "Error reading config file "+config_file+": " + str(e)
433 def do_vend_server():
434 options = parse_args()
435 config_opts = VendConfigFile(options.config_file, config_options)
438 dev_null = file('/dev/null')
439 fd = dev_null.fileno()
448 rfh, wfh = connect_to_vend(options, config_opts)
449 except (LATClientException, socket.error), e:
450 (exc_type, exc_value, exc_traceback) = sys.exc_info()
453 print "Connection error: "+str(exc_type)+" "+str(e)
454 print "Trying again in 5 seconds."
458 run_forever(rfh, wfh, options, config_opts)
459 except VendingException:
461 print "Connection died, trying again..."
463 if __name__ == '__main__':
464 def do_nothing(signum, stack): pass
465 def stop_server(signum, stack): raise KeyboardInterrupt
466 signal.signal(signal.SIGHUP, do_nothing)
467 signal.signal(signal.SIGTERM, stop_server)
468 signal.signal(signal.SIGINT, stop_server)
472 except KeyboardInterrupt:
473 print "Killed by signal."
475 except SystemExit, e:
478 (exc_type, exc_value, exc_traceback) = sys.exc_info()
480 print "Uh-oh, unhandled " + str(exc_type) + " exception"
481 print "Message: ", str(exc_value)
483 print_tb(exc_traceback)
486 print "This message should be considered a bug in the Vend Server."
487 print "Please report this to someone who can fix it."
490 print "Trying again anyway (might not help, but hey...)"