6 import sys, os, string, re, pwd
8 from time import time, sleep
9 from popen2 import popen2
10 from LATClient import LATClient, LATClientException
11 from VendingMachine import VendingMachine, VendingException
12 from HorizScroll import HorizScroll
13 from random import random, seed
14 from Idler import TrainIdler,GrayIdler
16 from traceback import print_tb
18 GREETING = 'UCC SNACKS'
25 class DispenseDatabase:
26 def __init__(self, vending_machine, host, name, user, password):
27 self.vending_machine = vending_machine
28 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
29 self.db.query('LISTEN vend_requests')
31 def process_requests(self):
33 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
35 outstanding = self.db.query(query).getresult()
36 except (pg.error,), db_err:
37 sys.stderr.write('Failed to query database: %s\n'%(db_err.strip()))
39 for (id, slot) in outstanding:
40 (worked, code, string) = self.vending_machine.vend(slot)
41 print (worked, code, string)
43 query = 'SELECT vend_success(%s)'%id
44 self.db.query(query).getresult()
46 query = 'SELECT vend_failed(%s)'%id
47 self.db.query(query).getresult()
49 def handle_events(self):
50 notifier = self.db.getnotify()
51 while notifier is not None:
52 self.process_requests()
53 notify = self.db.getnotify()
55 def scroll_options(username, mk, welcome = False):
57 msg = [(center('WELCOME'), False, 0.8),
58 (center(username), False, 0.8)]
61 choices = ' '*10+'CHOICES: '
63 coke_machine = file('/home/other/coke/coke_contents')
64 cokes = coke_machine.readlines()
71 (slot_num, price, slot_name) = c.split(' ', 2)
72 if slot_name == 'dead': continue
73 choices += '%s8-%s (%sc) '%(slot_num, slot_name, price)
75 choices += 'OR A SNACK. '
76 choices += '99 TO READ AGAIN. '
78 msg.append((choices, False, None))
83 info = pwd.getpwuid(uid)
86 if info.pw_dir == None: return False
87 pinfile = os.path.join(info.pw_dir, '.pin')
100 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
104 def has_good_pin(uid):
105 return get_pin(uid) != None
107 def verify_user_pin(uid, pin):
108 if get_pin(uid) == pin:
109 info = pwd.getpwuid(uid)
114 def door_open_mode(v):
115 print "Entering open door mode"
116 v.display("-FEED ME-")
122 if params == 1: # door closed
123 v.display("-YUM YUM!-")
129 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
130 choice = int(random()*len(messages))
131 msg = messages[choice]
132 left = range(len(msg))
133 for i in range(len(msg)):
134 if msg[i] == ' ': left.remove(i)
138 for i in range(0, len(msg)):
144 s += chr(int(random()*26)+ord('A'))
153 return ' '*((LEN-len(str))/2)+str
156 def __init__(self, vendie):
157 # Each element of scrolling_message should be a 3-tuple of
158 # ('message', True/False if it is to be repeated, time to display)
159 self.scrolling_message = []
161 self.next_update = None
163 def set_message(self, string):
164 self.scrolling_message = [(string, False, None)]
165 self.update_display(True)
167 def set_messages(self, strings):
168 self.scrolling_message = strings
169 self.update_display(True)
171 def update_display(self, forced = False):
172 if not forced and self.next_update != None and time() < self.next_update:
174 if len(self.scrolling_message) > 0:
175 if len(self.scrolling_message[0][0]) > 10:
176 (m, r, t) = self.scrolling_message[0]
178 exp = HorizScroll(m).expand(padding = 0, wraparound = True)
185 del self.scrolling_message[0]
186 self.scrolling_message = a + self.scrolling_message
187 newmsg = self.scrolling_message[0]
188 if newmsg[2] != None:
189 self.next_update = time() + newmsg[2]
191 self.next_update = None
192 self.v.display(self.scrolling_message[0][0])
193 if self.scrolling_message[0][1]:
194 self.scrolling_message.append(self.scrolling_message[0])
195 del self.scrolling_message[0]
198 return len(self.scrolling_message) == 0
200 def run_forever(rfh, wfh, options, cf):
201 v = VendingMachine(rfh, wfh)
202 print 'PING is', v.ping()
204 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
209 mk = MessageKeeper(v)
210 mk.set_message(GREETING)
211 time_to_autologout = None
212 #idler = TrainIdler(v)
213 #idler = GrayIdler(v)
214 idler = GrayIdler(v,one="*",zero="-")
216 last_timeout_refresh = None
219 if USE_DB: db.handle_events()
221 if time_to_autologout != None:
222 time_left = time_to_autologout - time()
223 if time_left < 6 and (last_timeout_refresh is None or last_timeout_refresh > time_left):
224 mk.set_message('LOGOUT: '+str(int(time_left)))
225 last_timeout_refresh = int(time_left)
228 if time_to_autologout != None and time_to_autologout - time() <= 0:
229 time_to_autologout = None
233 mk.set_message(GREETING)
235 if time_to_autologout and not mk.done(): time_to_autologout = None
236 if cur_user == '' and time_to_autologout: time_to_autologout = None
237 if len(cur_pin) == PIN_LENGTH and mk.done() and time_to_autologout == None:
239 time_to_autologout = time() + 15
241 if time_to_idle == None and cur_user == '': time_to_idle = time() + 60
242 if time_to_idle != None and cur_user != '': time_to_idle = None
243 if time_to_idle is not None and time() > time_to_idle: idler.next()
249 e = v.next_event(0.1)
260 mk.set_message(GREETING)
261 elif event == SWITCH:
262 # don't care right now.
266 # complicated key handling here:
267 if len(cur_user) < 5:
270 mk.set_message(GREETING)
272 cur_user += chr(key + ord('0'))
273 mk.set_message('UID: '+cur_user)
274 if len(cur_user) == 5:
276 if not has_good_pin(uid):
278 #[(center('INVALID'), False, 0.7),
279 #(center('PIN'), False, 0.7),
280 #(center('SETUP'), False, 1.0),
281 #(GREETING, False, None)])
283 [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
284 (GREETING, False, None)])
289 mk.set_message('PIN: ')
291 elif len(cur_pin) < PIN_LENGTH:
295 mk.set_message(GREETING)
298 mk.set_message('PIN: ')
300 cur_pin += chr(key + ord('0'))
301 mk.set_message('PIN: '+'X'*len(cur_pin))
302 if len(cur_pin) == PIN_LENGTH:
303 username = verify_user_pin(int(cur_user), int(cur_pin))
307 scroll_options(username, mk, True)
312 [(center('BAD PIN'), False, 1.0),
313 (center('SORRY'), False, 0.5),
314 (GREETING, False, None)])
318 elif len(cur_selection) == 0:
324 [(center('BYE!'), False, 1.5),
325 (GREETING, False, None)])
327 cur_selection += chr(key + ord('0'))
328 mk.set_message('SELECT: '+cur_selection)
329 time_to_autologout = None
330 elif len(cur_selection) == 1:
333 time_to_autologout = None
334 scroll_options(username, mk)
337 cur_selection += chr(key + ord('0'))
338 #make_selection(cur_selection)
339 # XXX this should move somewhere else:
340 if cur_selection == '55':
341 mk.set_message('OPENSESAME')
342 ret = os.system('su - "%s" -c "dispense door"'%username)
344 mk.set_message(center('DOOR OPEN'))
346 mk.set_message(center('BAD DOOR'))
348 elif cur_selection == '91':
350 elif cur_selection == '99':
351 scroll_options(username, mk)
354 elif cur_selection[1] == '8':
355 v.display('GOT COKE?')
356 os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
358 v.display('HERES A '+cur_selection)
359 v.vend(cur_selection)
361 v.display('THANK YOU')
364 time_to_autologout = time() + 8
366 def connect_to_vend(options, cf):
367 # Open vending machine via LAT?
369 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
370 rfh, wfh = latclient.get_fh()
372 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
374 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
375 sock.connect((options.host, options.port))
376 rfh = sock.makefile('r')
377 wfh = sock.makefile('w')
382 from optparse import OptionParser
384 op = OptionParser(usage="%prog [OPTION]...")
385 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')
386 op.add_option('-v', '--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
387 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
388 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
389 options, args = op.parse_args()
392 op.error('extra command line arguments: ' + ' '.join(args))
397 'DBServer': ('Database', 'Server'),
398 'DBName': ('Database', 'Name'),
399 'DBUser': ('VendingMachine', 'DBUser'),
400 'DBPassword': ('VendingMachine', 'DBPassword'),
402 'ServiceName': ('VendingMachine', 'ServiceName'),
403 'ServicePassword': ('VendingMachine', 'Password'),
405 'ServerName': ('DecServer', 'Name'),
406 'ConnectPassword': ('DecServer', 'ConnectPassword'),
407 'PrivPassword': ('DecServer', 'PrivPassword'),
410 class VendConfigFile:
411 def __init__(self, config_file, options):
413 cp = ConfigParser.ConfigParser()
416 for option in options:
417 section, name = options[option]
418 value = cp.get(section, name)
419 self.__dict__[option] = value
421 except ConfigParser.Error, e:
422 print "Error reading config file "+config_file+": " + str(e)
426 if __name__ == '__main__':
429 options = parse_args()
430 config_opts = VendConfigFile(options.config_file, config_options)
434 rfh, wfh = connect_to_vend(options, config_opts)
435 except (LATClientException, socket.error), e:
436 (exc_type, exc_value, exc_traceback) = sys.exc_info()
438 print "Connection error: "+str(exc_type)+" "+str(e)
439 print_tb(exc_traceback)
441 print "Trying again in 5 seconds."
445 run_forever(rfh, wfh, options, config_opts)
446 except VendingException:
448 print "Connection died, trying again..."