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):
201 v = VendingMachine(rfh, wfh)
202 print 'PING is', v.ping()
204 if USE_DB: db = DispenseDatabase(v, DBServer, DBName, DBUser, 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 # FIXME: a less ugly way of passing all these options would be nice
367 def connect_to_vend(options, DBServer, DBName, DBUser, DBPassword, ServiceName, ServicePassword, ServerName, ConnectPassword, PrivPassword):
368 # Open vending machine via LAT
370 latclient = LATClient(service = ServiceName, password = ServicePassword, server_name = ServerName, connect_password = ConnectPassword, priv_password = PrivPassword)
371 rfh, wfh = latclient.get_fh()
373 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
375 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
376 sock.connect((options.host, options.port))
377 rfh = sock.makefile('r')
378 wfh = sock.makefile('w')
382 if __name__ == '__main__':
383 from ConfigParser import ConfigParser
384 from optparse import OptionParser
386 op = OptionParser(usage="%prog [OPTION]...")
387 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')
388 op.add_option('-v', '--virtualvend', action='store_false', default=True, dest='use_lat', help='use the virtual vending server instead of LAT')
389 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
390 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
391 options, args = op.parse_args()
394 op.error('extra command line arguments: ' + ' '.join(args))
397 cp.read(options.config_file)
398 DBServer = cp.get('Database', 'Server')
399 DBName = cp.get('Database', 'Name')
400 DBUser = cp.get('VendingMachine', 'DBUser')
401 DBPassword = cp.get('VendingMachine', 'DBPassword')
403 ServiceName = cp.get('VendingMachine', 'ServiceName')
404 ServicePassword = cp.get('VendingMachine', 'Password')
406 ServerName = cp.get('DecServer', 'Name')
407 ConnectPassword = cp.get('DecServer', 'ConnectPassword')
408 PrivPassword = cp.get('DecServer', 'PrivPassword')
412 rfh, wfh = connect_to_vend(options, DBServer, DBName, DBUser, DBPassword, ServiceName, ServicePassword, ServerName, ConnectPassword, PrivPassword)
413 except (LATClientException, socket.error), e:
414 (exc_type, exc_value, exc_traceback) = sys.exc_info()
416 print "Connection error: "+str(exc_type)+" "+str(e)
417 print_tb(exc_traceback)
419 print "Trying again in 5 seconds."
423 run_forever(rfh, wfh)
424 except VendingException:
426 print "Connection died, trying again..."