d8122aa0507af0b8a218fed5b399db2f40be6c95
[uccvend-vendserver.git] / sql-edition / servers / VendServer.py
1 #!/usr/bin/python
2 # vim:ts=4
3
4 USE_DB = 0
5
6 import sys, os, string, re, pwd
7 if USE_DB: import pg
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
15 import socket
16 from traceback import print_tb
17
18 GREETING = 'UCC SNACKS'
19 PIN_LENGTH = 4
20
21 DOOR = 1
22 SWITCH = 2
23 KEY = 3
24
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')
30
31         def process_requests(self):
32                 print 'processing'
33                 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
34                 try:
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()))
38                         return
39                 for (id, slot) in outstanding:
40                         (worked, code, string) = self.vending_machine.vend(slot)
41                         print (worked, code, string)
42                         if worked:
43                                 query = 'SELECT vend_success(%s)'%id
44                                 self.db.query(query).getresult()
45                         else:
46                                 query = 'SELECT vend_failed(%s)'%id
47                                 self.db.query(query).getresult()
48
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()
54
55 def scroll_options(username, mk, welcome = False):
56         if welcome:
57                 msg = [(center('WELCOME'), False, 0.8),
58                            (center(username), False, 0.8)]
59         else:
60                 msg = []
61         choices = ' '*10+'CHOICES: '
62         try:
63                 coke_machine = file('/home/other/coke/coke_contents')
64                 cokes = coke_machine.readlines()
65                 coke_machine.close()
66         except:
67                 cokes = []
68                 pass
69         for c in cokes:
70                 c = c.strip()
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)
74         choices += '55-DOOR '
75         choices += 'OR A SNACK. '
76         choices += '99 TO READ AGAIN. '
77         choices += 'CHOICE?   '
78         msg.append((choices, False, None))
79         mk.set_messages(msg)
80
81 def get_pin(uid):
82         try:
83                 info = pwd.getpwuid(uid)
84         except KeyError:
85                 return None
86         if info.pw_dir == None: return False
87         pinfile = os.path.join(info.pw_dir, '.pin')
88         try:
89                 s = os.stat(pinfile)
90         except OSError:
91                 return None
92         if s.st_mode & 077:
93                 return None
94         try:
95                 f = file(pinfile)
96         except IOError:
97                 return None
98         pinstr = f.readline()
99         f.close()
100         if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
101                 return None
102         return int(pinstr)
103
104 def has_good_pin(uid):
105         return get_pin(uid) != None
106
107 def verify_user_pin(uid, pin):
108         if get_pin(uid) == pin:
109                 info = pwd.getpwuid(uid)
110                 return info.pw_name
111         else:
112                 return None
113
114 def door_open_mode(v):
115         print "Entering open door mode"
116         v.display("-FEED  ME-")
117         while True:
118                 e = v.next_event()
119                 if e == None: break
120                 (event, params) = e
121                 if event == DOOR:
122                         if params == 1: # door closed
123                                 v.display("-YUM YUM!-")
124                                 sleep(1)
125                                 return
126
127 def cookie(v):
128         seed(time())
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)
135         reveal = 1
136         while left:
137                 s = ''
138                 for i in range(0, len(msg)):
139                         if i in left:
140                                 if reveal == 0:
141                                         left.remove(i)
142                                         s += msg[i]
143                                 else:
144                                         s += chr(int(random()*26)+ord('A'))
145                                 reveal += 1
146                                 reveal %= 17
147                         else:
148                                 s += msg[i]
149                 v.display(s)
150
151 def center(str):
152         LEN = 10
153         return ' '*((LEN-len(str))/2)+str
154
155 class MessageKeeper:
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 = []
160                 self.v = vendie
161                 self.next_update = None
162
163         def set_message(self, string):
164                 self.scrolling_message = [(string, False, None)]
165                 self.update_display(True)
166
167         def set_messages(self, strings):
168                 self.scrolling_message = strings
169                 self.update_display(True)
170
171         def update_display(self, forced = False):
172                 if not forced and self.next_update != None and time() < self.next_update:
173                         return
174                 if len(self.scrolling_message) > 0:
175                         if len(self.scrolling_message[0][0]) > 10:
176                                 (m, r, t) = self.scrolling_message[0]
177                                 a = []
178                                 exp = HorizScroll(m).expand(padding = 0, wraparound = True)
179                                 if t == None:
180                                         t = 0.1
181                                 else:
182                                         t = t / len(exp)
183                                 for x in exp:
184                                         a.append((x, r, t))
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]
190                         else:
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]
196
197         def done(self):
198                 return len(self.scrolling_message) == 0
199
200 def run_forever(rfh, wfh):
201         v = VendingMachine(rfh, wfh)
202         print 'PING is', v.ping()
203
204         if USE_DB: db = DispenseDatabase(v, DBServer, DBName, DBUser, DBPassword)
205         cur_user = ''
206         cur_pin = ''
207         cur_selection = ''
208
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="-")
215         time_to_idle = None
216         last_timeout_refresh = None
217
218         while True:
219                 if USE_DB: db.handle_events()
220
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)
226                                 cur_selection = ''
227
228                 if time_to_autologout != None and time_to_autologout - time() <= 0:
229                         time_to_autologout = None
230                         cur_user = ''
231                         cur_pin = ''
232                         cur_selection = ''
233                         mk.set_message(GREETING)
234
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:
238                         # start autologout
239                         time_to_autologout = time() + 15
240
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()
244
245                 mk.update_display()
246
247                 e = v.next_event(0)
248                 if e == None:
249                         e = v.next_event(0.1)
250                         if e == None:
251                                 continue
252                 time_to_idle = None
253                 (event, params) = e
254                 print e
255                 if event == DOOR:
256                         if params == 0:
257                                 door_open_mode(v);
258                                 cur_user = ''
259                                 cur_pin = ''
260                                 mk.set_message(GREETING)
261                 elif event == SWITCH:
262                         # don't care right now.
263                         pass
264                 elif event == KEY:
265                         key = params
266                         # complicated key handling here:
267                         if len(cur_user) < 5:
268                                 if key == 11:
269                                         cur_user = ''
270                                         mk.set_message(GREETING)
271                                         continue
272                                 cur_user += chr(key + ord('0'))
273                                 mk.set_message('UID: '+cur_user)
274                                 if len(cur_user) == 5:
275                                         uid = int(cur_user)
276                                         if not has_good_pin(uid):
277                                                 #mk.set_messages(
278                                                         #[(center('INVALID'), False, 0.7),
279                                                          #(center('PIN'), False, 0.7),
280                                                          #(center('SETUP'), False, 1.0),
281                                                          #(GREETING, False, None)])
282                                                 mk.set_messages(
283                                                         [(' '*10+'INVALID PIN SETUP'+' '*10, False, 3),
284                                                          (GREETING, False, None)])
285                                                 cur_user = ''
286                                                 cur_pin = ''
287                                                 continue
288                                         cur_pin = ''
289                                         mk.set_message('PIN: ')
290                                         continue
291                         elif len(cur_pin) < PIN_LENGTH:
292                                 if key == 11:
293                                         if cur_pin == '':
294                                                 cur_user = ''
295                                                 mk.set_message(GREETING)
296                                                 continue
297                                         cur_pin = ''
298                                         mk.set_message('PIN: ')
299                                         continue
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))
304                                         if username:
305                                                 v.beep(0, False)
306                                                 cur_selection = ''
307                                                 scroll_options(username, mk, True)
308                                                 continue
309                                         else:
310                                                 v.beep(40, False)
311                                                 mk.set_messages(
312                                                         [(center('BAD PIN'), False, 1.0),
313                                                          (center('SORRY'), False, 0.5),
314                                                          (GREETING, False, None)])
315                                                 cur_user = ''
316                                                 cur_pin = ''
317                                                 continue
318                         elif len(cur_selection) == 0:
319                                 if key == 11:
320                                         cur_pin = ''
321                                         cur_user = ''
322                                         cur_selection = ''
323                                         mk.set_messages(
324                                                 [(center('BYE!'), False, 1.5),
325                                                  (GREETING, False, None)])
326                                         continue
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:
331                                 if key == 11:
332                                         cur_selection = ''
333                                         time_to_autologout = None
334                                         scroll_options(username, mk)
335                                         continue
336                                 else:
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)
343                                                 if ret == 0:
344                                                         mk.set_message(center('DOOR OPEN'))
345                                                 else:
346                                                         mk.set_message(center('BAD DOOR'))
347                                                 sleep(1)
348                                         elif cur_selection == '91':
349                                                 cookie(v)
350                                         elif cur_selection == '99':
351                                                 scroll_options(username, mk)
352                                                 cur_selection = ''
353                                                 continue
354                                         elif cur_selection[1] == '8':
355                                                 v.display('GOT COKE?')
356                                                 os.system('su - "%s" -c "dispense %s"'%(username, cur_selection[0]))
357                                         else:
358                                                 v.display('HERES A '+cur_selection)
359                                                 v.vend(cur_selection)
360                                         sleep(0.5)
361                                         v.display('THANK YOU')
362                                         sleep(0.5)
363                                         cur_selection = ''
364                                         time_to_autologout = time() + 8
365
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
369         if options.use_lat:
370                 latclient = LATClient(service = ServiceName, password = ServicePassword, server_name = ServerName, connect_password = ConnectPassword, priv_password = PrivPassword)
371                 rfh, wfh = latclient.get_fh()
372         else:
373                 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
374                 import socket
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')
379                 
380         return rfh, wfh
381
382 if __name__ == '__main__':
383         from ConfigParser import ConfigParser
384         from optparse import OptionParser
385
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()
392
393         if len(args) != 0:
394                 op.error('extra command line arguments: ' + ' '.join(args))
395
396         cp = ConfigParser()
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')
402
403         ServiceName = cp.get('VendingMachine', 'ServiceName')
404         ServicePassword = cp.get('VendingMachine', 'Password')
405
406         ServerName = cp.get('DecServer', 'Name')
407         ConnectPassword = cp.get('DecServer', 'ConnectPassword')
408         PrivPassword = cp.get('DecServer', 'PrivPassword')
409
410         while True:
411                 try:
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()
415                         print
416                         print "Connection error: "+str(exc_type)+" "+str(e)
417                         print_tb(exc_traceback)
418                         del exc_traceback
419                         print "Trying again in 5 seconds."
420                         sleep(5)
421                         continue
422                 try:
423                         run_forever(rfh, wfh)
424                 except VendingException:
425                         print
426                         print "Connection died, trying again..."
427

UCC git Repository :: git.ucc.asn.au