7 import sys, os, string, re, pwd
9 from time import time, sleep
10 from popen2 import popen2
11 from LATClient import LATClient, LATClientException
12 from VendingMachine import VendingMachine, VendingException
13 from HorizScroll import HorizScroll
14 from random import random, seed
15 from Idler import TrainIdler,GrayIdler
17 from posix import geteuid
19 GREETING = 'UCC SNACKS'
26 class DispenseDatabase:
27 def __init__(self, vending_machine, host, name, user, password):
28 self.vending_machine = vending_machine
29 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
30 self.db.query('LISTEN vend_requests')
32 def process_requests(self):
34 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
36 outstanding = self.db.query(query).getresult()
37 except (pg.error,), db_err:
38 sys.stderr.write('Failed to query database: %s\n'%(db_err.strip()))
40 for (id, slot) in outstanding:
41 (worked, code, string) = self.vending_machine.vend(slot)
42 print (worked, code, string)
44 query = 'SELECT vend_success(%s)'%id
45 self.db.query(query).getresult()
47 query = 'SELECT vend_failed(%s)'%id
48 self.db.query(query).getresult()
50 def handle_events(self):
51 notifier = self.db.getnotify()
52 while notifier is not None:
53 self.process_requests()
54 notify = self.db.getnotify()
56 def scroll_options(username, mk, welcome = False):
58 msg = [(center('WELCOME'), False, 0.8),
59 (center(username), False, 0.8)]
62 choices = ' '*10+'CHOICES: '
64 coke_machine = file('/home/other/coke/coke_contents')
65 cokes = coke_machine.readlines()
72 (slot_num, price, slot_name) = c.split(' ', 2)
73 if slot_name == 'dead': continue
74 choices += '%s8-%s (%sc) '%(slot_num, slot_name, price)
76 choices += 'OR A SNACK. '
77 choices += '99 TO READ AGAIN. '
79 msg.append((choices, False, None))
84 info = pwd.getpwuid(uid)
87 if info.pw_dir == None: return False
88 pinfile = os.path.join(info.pw_dir, '.pin')
101 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
105 def has_good_pin(uid):
106 return get_pin(uid) != None
108 def verify_user_pin(uid, pin):
109 if get_pin(uid) == pin:
110 info = pwd.getpwuid(uid)
115 def door_open_mode(v):
116 print "Entering open door mode"
117 v.display("-FEED ME-")
123 if params == 1: # door closed
124 v.display("-YUM YUM!-")
130 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
131 choice = int(random()*len(messages))
132 msg = messages[choice]
133 left = range(len(msg))
134 for i in range(len(msg)):
135 if msg[i] == ' ': left.remove(i)
139 for i in range(0, len(msg)):
145 s += chr(int(random()*26)+ord('A'))
154 return ' '*((LEN-len(str))/2)+str
157 def __init__(self, vendie):
158 # Each element of scrolling_message should be a 3-tuple of
159 # ('message', True/False if it is to be repeated, time to display)
160 self.scrolling_message = []
162 self.next_update = None
164 def set_message(self, string):
165 self.scrolling_message = [(string, False, None)]
166 self.update_display(True)
168 def set_messages(self, strings):
169 self.scrolling_message = strings
170 self.update_display(True)
172 def update_display(self, forced = False):
173 if not forced and self.next_update != None and time() < self.next_update:
175 if len(self.scrolling_message) > 0:
176 if len(self.scrolling_message[0][0]) > 10:
177 (m, r, t) = self.scrolling_message[0]
179 exp = HorizScroll(m).expand(padding = 0, wraparound = True)
186 del self.scrolling_message[0]
187 self.scrolling_message = a + self.scrolling_message
188 newmsg = self.scrolling_message[0]
189 if newmsg[2] != None:
190 self.next_update = time() + newmsg[2]
192 self.next_update = None
193 self.v.display(self.scrolling_message[0][0])
194 if self.scrolling_message[0][1]:
195 self.scrolling_message.append(self.scrolling_message[0])
196 del self.scrolling_message[0]
199 return len(self.scrolling_message) == 0
201 def run_forever(rfh, wfh, options, cf):
202 v = VendingMachine(rfh, wfh)
203 print 'PING is', v.ping()
205 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
210 mk = MessageKeeper(v)
211 mk.set_message(GREETING)
212 time_to_autologout = None
213 #idler = TrainIdler(v)
214 #idler = GrayIdler(v)
215 idler = GrayIdler(v,one="*",zero="-")
217 last_timeout_refresh = None
220 if USE_DB: db.handle_events()
222 if time_to_autologout != None:
223 time_left = time_to_autologout - time()
224 if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
225 mk.set_message('LOGOUT: '+str(int(time_left)))
226 last_timeout_refresh = int(time_left)
229 if time_to_autologout != None and time_to_autologout - time() <= 0:
230 time_to_autologout = None
234 mk.set_message(GREETING)
236 if time_to_autologout and not mk.done(): time_to_autologout = None
237 if cur_user == '' and time_to_autologout: time_to_autologout = None
238 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
240 time_to_autologout = time() + 15
242 if time_to_idle == None and cur_user == '': time_to_idle = time() + 60
243 if time_to_idle != None and cur_user != '': time_to_idle = None
244 if time_to_idle is not None and time() > time_to_idle: idler.next()
250 e = v.next_event(0.1)
261 mk.set_message(GREETING)
262 elif event == SWITCH:
263 # don't care right now.
267 # complicated key handling here:
268 if len(cur_user) < 5:
271 mk.set_message(GREETING)
273 cur_user += chr(key + ord('0'))
274 mk.set_message('UID: '+cur_user)
275 if len(cur_user) == 5:
277 if not has_good_pin(uid):
279 #[(center('INVALID'), False, 0.7),
280 #(center('PIN'), False, 0.7),
281 #(center('SETUP'), False, 1.0),
282 #(GREETING, False, None)])
284 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
285 (GREETING, False, None)])
290 mk.set_message('PIN: ')
292 elif len(cur_pin) < PIN_LENGTH:
296 mk.set_message(GREETING)
299 mk.set_message('PIN: ')
301 cur_pin += chr(key + ord('0'))
302 mk.set_message('PIN: '+'X'*len(cur_pin))
303 if len(cur_pin) == PIN_LENGTH:
304 username = verify_user_pin(int(cur_user), int(cur_pin))
308 scroll_options(username, mk, True)
313 [(center('BAD PIN'), False, 1.0),
314 (center('SORRY'), False, 0.5),
315 (GREETING, False, None)])
319 elif len(cur_selection) == 0:
325 [(center('BYE!'), False, 1.5),
326 (GREETING, False, None)])
328 cur_selection += chr(key + ord('0'))
329 mk.set_message('SELECT: '+cur_selection)
330 time_to_autologout = None
331 elif len(cur_selection) == 1:
334 time_to_autologout = None
335 scroll_options(username, mk)
338 cur_selection += chr(key + ord('0'))
339 #make_selection(cur_selection)
340 # XXX this should move somewhere else:
341 if cur_selection == '55':
342 mk.set_message('OPENSESAME')
344 ret = os.system('su - "%s" -c "dispense door"'%username)
346 ret = os.system('dispense door')
348 mk.set_message(center('DOOR OPEN'))
350 mk.set_message(center('BAD DOOR'))
352 elif cur_selection == '91':
354 elif cur_selection == '99':
355 scroll_options(username, mk)
358 elif cur_selection[1] == '8':
359 v.display('GOT COKE?')
360 os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
362 v.display('HERES A '+cur_selection)
363 v.vend(cur_selection)
365 v.display('THANK YOU')
368 time_to_autologout = time() + 8
370 def connect_to_vend(options, cf):
371 # Open vending machine via LAT?
373 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
374 rfh, wfh = latclient.get_fh()
376 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
378 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
379 sock.connect((options.host, options.port))
380 rfh = sock.makefile('r')
381 wfh = sock.makefile('w')
386 from optparse import OptionParser
388 op = OptionParser(usage="%prog [OPTION]...")
389 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')
390 op.add_option('-v', '--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
391 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
392 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
393 options, args = op.parse_args()
396 op.error('extra command line arguments: ' + ' '.join(args))
401 'DBServer': ('Database', 'Server'),
402 'DBName': ('Database', 'Name'),
403 'DBUser': ('VendingMachine', 'DBUser'),
404 'DBPassword': ('VendingMachine', 'DBPassword'),
406 'ServiceName': ('VendingMachine', 'ServiceName'),
407 'ServicePassword': ('VendingMachine', 'Password'),
409 'ServerName': ('DecServer', 'Name'),
410 'ConnectPassword': ('DecServer', 'ConnectPassword'),
411 'PrivPassword': ('DecServer', 'PrivPassword'),
414 class VendConfigFile:
415 def __init__(self, config_file, options):
417 cp = ConfigParser.ConfigParser()
420 for option in options:
421 section, name = options[option]
422 value = cp.get(section, name)
423 self.__dict__[option] = value
425 except ConfigParser.Error, e:
426 print "Error reading config file "+config_file+": " + str(e)
429 def do_vend_server():
430 options = parse_args()
431 config_opts = VendConfigFile(options.config_file, config_options)
435 rfh, wfh = connect_to_vend(options, config_opts)
436 except (LATClientException, socket.error), e:
437 (exc_type, exc_value, exc_traceback) = sys.exc_info()
440 print "Connection error: "+str(exc_type)+" "+str(e)
441 print "Trying again in 5 seconds."
445 run_forever(rfh, wfh, options, config_opts)
446 except VendingException:
448 print "Connection died, trying again..."
451 if __name__ == '__main__':
455 except KeyboardInterrupt:
456 print "Killed by SIGINT."
459 (exc_type, exc_value, exc_traceback) = sys.exc_info()
461 print "Uh-oh, unhandled " + str(exc_type) + " exception"
462 print "Message: ", str(exc_value)
464 print_tb(exc_traceback)
467 print "This message should be considered a bug in the Vend Server."
468 print "Please report this to someone who can fix it."
470 print "Trying again anyway (might not help, but hey...)"