7 import sys, os, string, re, pwd, signal
9 from traceback import print_tb
11 from time import time, sleep
12 from popen2 import popen2
13 from LATClient import LATClient, LATClientException
14 from VendingMachine import VendingMachine, VendingException
15 from HorizScroll import HorizScroll
16 from random import random, seed
17 from Idler import TrainIdler,GrayIdler
19 from posix import geteuid
21 GREETING = 'UCC SNACKS'
28 class DispenseDatabase:
29 def __init__(self, vending_machine, host, name, user, password):
30 self.vending_machine = vending_machine
31 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
32 self.db.query('LISTEN vend_requests')
34 def process_requests(self):
36 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
38 outstanding = self.db.query(query).getresult()
39 except (pg.error,), db_err:
40 sys.stderr.write('Failed to query database: %s\n'%(db_err.strip()))
42 for (id, slot) in outstanding:
43 (worked, code, string) = self.vending_machine.vend(slot)
44 print (worked, code, string)
46 query = 'SELECT vend_success(%s)'%id
47 self.db.query(query).getresult()
49 query = 'SELECT vend_failed(%s)'%id
50 self.db.query(query).getresult()
52 def handle_events(self):
53 notifier = self.db.getnotify()
54 while notifier is not None:
55 self.process_requests()
56 notify = self.db.getnotify()
58 def scroll_options(username, mk, welcome = False):
60 msg = [(center('WELCOME'), False, 0.8),
61 (center(username), False, 0.8)]
64 choices = ' '*10+'CHOICES: '
66 coke_machine = file('/home/other/coke/coke_contents')
67 cokes = coke_machine.readlines()
74 (slot_num, price, slot_name) = c.split(' ', 2)
75 if slot_name == 'dead': continue
76 choices += '%s8-%s (%sc) '%(slot_num, slot_name, price)
78 choices += 'OR A SNACK. '
79 choices += '99 TO READ AGAIN. '
81 msg.append((choices, False, None))
86 info = pwd.getpwuid(uid)
89 if info.pw_dir == None: return False
90 pinfile = os.path.join(info.pw_dir, '.pin')
101 pinstr = f.readline()
103 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
107 def has_good_pin(uid):
108 return get_pin(uid) != None
110 def verify_user_pin(uid, pin):
111 if get_pin(uid) == pin:
112 info = pwd.getpwuid(uid)
117 def door_open_mode(v):
118 print "Entering open door mode"
119 v.display("-FEED ME-")
125 if params == 1: # door closed
126 v.display("-YUM YUM!-")
132 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
133 choice = int(random()*len(messages))
134 msg = messages[choice]
135 left = range(len(msg))
136 for i in range(len(msg)):
137 if msg[i] == ' ': left.remove(i)
141 for i in range(0, len(msg)):
147 s += chr(int(random()*26)+ord('A'))
156 return ' '*((LEN-len(str))/2)+str
159 def __init__(self, vendie):
160 # Each element of scrolling_message should be a 3-tuple of
161 # ('message', True/False if it is to be repeated, time to display)
162 self.scrolling_message = []
164 self.next_update = None
166 def set_message(self, string):
167 self.scrolling_message = [(string, False, None)]
168 self.update_display(True)
170 def set_messages(self, strings):
171 self.scrolling_message = strings
172 self.update_display(True)
174 def update_display(self, forced = False):
175 if not forced and self.next_update != None and time() < self.next_update:
177 if len(self.scrolling_message) > 0:
178 if len(self.scrolling_message[0][0]) > 10:
179 (m, r, t) = self.scrolling_message[0]
181 exp = HorizScroll(m).expand(padding = 0, wraparound = True)
188 del self.scrolling_message[0]
189 self.scrolling_message = a + self.scrolling_message
190 newmsg = self.scrolling_message[0]
191 if newmsg[2] != None:
192 self.next_update = time() + newmsg[2]
194 self.next_update = None
195 self.v.display(self.scrolling_message[0][0])
196 if self.scrolling_message[0][1]:
197 self.scrolling_message.append(self.scrolling_message[0])
198 del self.scrolling_message[0]
201 return len(self.scrolling_message) == 0
203 def run_forever(rfh, wfh, options, cf):
204 v = VendingMachine(rfh, wfh)
205 print 'PING is', v.ping()
207 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
212 mk = MessageKeeper(v)
213 mk.set_message(GREETING)
214 time_to_autologout = None
215 #idler = TrainIdler(v)
216 #idler = GrayIdler(v)
217 idler = GrayIdler(v,one="*",zero="-")
219 last_timeout_refresh = None
222 if USE_DB: db.handle_events()
224 if time_to_autologout != None:
225 time_left = time_to_autologout - time()
226 if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
227 mk.set_message('LOGOUT: '+str(int(time_left)))
228 last_timeout_refresh = int(time_left)
231 if time_to_autologout != None and time_to_autologout - time() <= 0:
232 time_to_autologout = None
236 mk.set_message(GREETING)
238 if time_to_autologout and not mk.done(): time_to_autologout = None
239 if cur_user == '' and time_to_autologout: time_to_autologout = None
240 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
242 time_to_autologout = time() + 15
244 if time_to_idle == None and cur_user == '': time_to_idle = time() + 60
245 if time_to_idle != None and cur_user != '': time_to_idle = None
246 if time_to_idle is not None and time() > time_to_idle: idler.next()
252 e = v.next_event(0.1)
263 mk.set_message(GREETING)
264 elif event == SWITCH:
265 # don't care right now.
269 # complicated key handling here:
270 if len(cur_user) < 5:
273 mk.set_message(GREETING)
275 cur_user += chr(key + ord('0'))
276 mk.set_message('UID: '+cur_user)
277 if len(cur_user) == 5:
279 if not has_good_pin(uid):
281 #[(center('INVALID'), False, 0.7),
282 #(center('PIN'), False, 0.7),
283 #(center('SETUP'), False, 1.0),
284 #(GREETING, False, None)])
286 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
287 (GREETING, False, None)])
292 mk.set_message('PIN: ')
294 elif len(cur_pin) < PIN_LENGTH:
298 mk.set_message(GREETING)
301 mk.set_message('PIN: ')
303 cur_pin += chr(key + ord('0'))
304 mk.set_message('PIN: '+'X'*len(cur_pin))
305 if len(cur_pin) == PIN_LENGTH:
306 username = verify_user_pin(int(cur_user), int(cur_pin))
310 scroll_options(username, mk, True)
315 [(center('BAD PIN'), False, 1.0),
316 (center('SORRY'), False, 0.5),
317 (GREETING, False, None)])
321 elif len(cur_selection) == 0:
327 [(center('BYE!'), False, 1.5),
328 (GREETING, False, None)])
330 cur_selection += chr(key + ord('0'))
331 mk.set_message('SELECT: '+cur_selection)
332 time_to_autologout = None
333 elif len(cur_selection) == 1:
336 time_to_autologout = None
337 scroll_options(username, mk)
340 cur_selection += chr(key + ord('0'))
341 #make_selection(cur_selection)
342 # XXX this should move somewhere else:
343 if cur_selection == '55':
344 mk.set_message('OPENSESAME')
346 ret = os.system('su - "%s" -c "dispense door"'%username)
348 ret = os.system('dispense door')
350 mk.set_message(center('DOOR OPEN'))
352 mk.set_message(center('BAD DOOR'))
354 elif cur_selection == '91':
356 elif cur_selection == '99':
357 scroll_options(username, mk)
360 elif cur_selection[1] == '8':
361 v.display('GOT COKE?')
362 os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
364 v.display('HERES A '+cur_selection)
365 v.vend(cur_selection)
367 v.display('THANK YOU')
370 time_to_autologout = time() + 8
372 def connect_to_vend(options, cf):
373 # Open vending machine via LAT?
375 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
376 rfh, wfh = latclient.get_fh()
378 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
380 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
381 sock.connect((options.host, options.port))
382 rfh = sock.makefile('r')
383 wfh = sock.makefile('w')
388 from optparse import OptionParser
390 op = OptionParser(usage="%prog [OPTION]...")
391 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')
392 op.add_option('-v', '--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
393 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
394 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
395 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
396 op.add_option('-s', '--syslog', dest='syslog', action='store_true', default=False, help='log output to syslog')
397 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
398 options, args = op.parse_args()
401 op.error('extra command line arguments: ' + ' '.join(args))
406 'DBServer': ('Database', 'Server'),
407 'DBName': ('Database', 'Name'),
408 'DBUser': ('VendingMachine', 'DBUser'),
409 'DBPassword': ('VendingMachine', 'DBPassword'),
411 'ServiceName': ('VendingMachine', 'ServiceName'),
412 'ServicePassword': ('VendingMachine', 'Password'),
414 'ServerName': ('DecServer', 'Name'),
415 'ConnectPassword': ('DecServer', 'ConnectPassword'),
416 'PrivPassword': ('DecServer', 'PrivPassword'),
419 class VendConfigFile:
420 def __init__(self, config_file, options):
422 cp = ConfigParser.ConfigParser()
425 for option in options:
426 section, name = options[option]
427 value = cp.get(section, name)
428 self.__dict__[option] = value
430 except ConfigParser.Error, e:
431 print "Error reading config file "+config_file+": " + str(e)
435 def do_nothing(signum, stack): pass
436 def stop_server(signum, stack): raise KeyboardInterrupt
437 signal.signal(signal.SIGHUP, do_nothing)
438 signal.signal(signal.SIGTERM, stop_server)
439 signal.signal(signal.SIGINT, stop_server)
441 options = parse_args()
442 config_opts = VendConfigFile(options.config_file, config_options)
443 set_up_logging(options)
444 if options.daemon: become_daemon()
446 return options, config_opts
448 def become_daemon(options):
449 dev_null = file('/dev/null')
450 fd = dev_null.fileno()
457 def do_vend_server(options, config_opts):
460 rfh, wfh = connect_to_vend(options, config_opts)
461 except (LATClientException, socket.error), e:
462 (exc_type, exc_value, exc_traceback) = sys.exc_info()
465 print "Connection error: "+str(exc_type)+" "+str(e)
466 print "Trying again in 5 seconds."
470 run_forever(rfh, wfh, options, config_opts)
471 except VendingException:
473 print "Connection died, trying again..."
475 if __name__ == '__main__':
476 options, config_opts = set_stuff_up()
479 do_vend_server(options, config_opts)
480 except KeyboardInterrupt:
481 print "Killed by signal."
483 except SystemExit, e:
486 (exc_type, exc_value, exc_traceback) = sys.exc_info()
488 print "Uh-oh, unhandled " + str(exc_type) + " exception"
489 print "Message: ", str(exc_value)
491 print_tb(exc_traceback)
494 print "This message should be considered a bug in the Vend Server."
495 print "Please report this to someone who can fix it."
498 print "Trying again anyway (might not help, but hey...)"