Modify idler selection algorithm
[uccvend-vendserver.git] / VendServer / VendServer.py
1 #!/usr/bin/python
2 # vim:ts=4
3
4 USE_DB = 0
5 USE_MIFARE = 1
6
7 import ConfigParser
8 import sys, os, string, re, pwd, signal, math, syslog
9 import logging, logging.handlers
10 from traceback import format_tb
11 if USE_DB: import pg
12 from time import time, sleep, mktime, localtime
13 from subprocess import Popen, PIPE
14 from LATClient import LATClient, LATClientException
15 from SerialClient import SerialClient, SerialClientException
16 from VendingMachine import VendingMachine, VendingException
17 from MessageKeeper import MessageKeeper
18 from HorizScroll import HorizScroll
19 from random import random, seed
20 from Idler import GreetingIdler,TrainIdler,GrayIdler,StringIdler,ClockIdler,FortuneIdler,FileIdler,PipeIdler
21 from SnackConfig import get_snack#, get_snacks
22 import socket
23 from posix import geteuid
24 from LDAPConnector import get_uid,get_uname, set_card_id
25
26 CREDITS="""
27 This vending machine software brought to you by:
28 Bernard Blackham
29 Mark Tearle
30 Nick Bannon
31 Cameron Patrick
32 and a collective of hungry alpacas.
33
34
35
36 For a good time call +61 8 6488 3901
37
38
39
40 """
41
42 PIN_LENGTH = 4
43
44 DOOR = 1
45 SWITCH = 2
46 KEY = 3
47 TICK = 4
48 MIFARE = 5
49
50
51 (
52 STATE_IDLE,
53 STATE_DOOR_OPENING,
54 STATE_DOOR_CLOSING,
55 STATE_GETTING_UID,
56 STATE_GETTING_PIN,
57 STATE_GET_SELECTION,
58 STATE_GRANDFATHER_CLOCK,
59 ) = range(1,8)
60
61 TEXT_SPEED = 0.8
62 IDLE_SPEED = 0.05
63
64 class DispenseDatabaseException(Exception): pass
65
66 class DispenseDatabase:
67         def __init__(self, vending_machine, host, name, user, password):
68                 self.vending_machine = vending_machine
69                 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
70                 self.db.query('LISTEN vend_requests')
71
72         def process_requests(self):
73                 logging.debug('database processing')
74                 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
75                 try:
76                         outstanding = self.db.query(query).getresult()
77                 except (pg.error,), db_err:
78                         raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
79                 for (id, slot) in outstanding:
80                         (worked, code, string) = self.vending_machine.vend(slot)
81                         logging.debug (str((worked, code, string)))
82                         if worked:
83                                 query = 'SELECT vend_success(%s)'%id
84                                 self.db.query(query).getresult()
85                         else:
86                                 query = 'SELECT vend_failed(%s)'%id
87                                 self.db.query(query).getresult()
88
89         def handle_events(self):
90                 notifier = self.db.getnotify()
91                 while notifier is not None:
92                         self.process_requests()
93                         notify = self.db.getnotify()
94
95 def scroll_options(username, mk, welcome = False):
96         if welcome:
97                 # Balance checking
98                 acct, unused = Popen(['dispense', 'acct', username], close_fds=True, stdout=PIPE).communicate()
99                 # this is fucking appalling
100                 balance = acct[acct.find("$")+1:acct.find("(")].strip()
101         
102                 msg = [(center('WELCOME'), False, TEXT_SPEED),
103                            (center(username), False, TEXT_SPEED),
104                            (center(balance), False, TEXT_SPEED),]
105         else:
106                 msg = []
107         choices = ' '*10+'CHOICES: '
108
109         # Get coke contents
110         cokes = []
111         for i in range(0, 7):
112                 args = ('dispense', 'iteminfo', 'coke:%i' % i)
113                 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
114                 m = re.match("\s*[a-z]+:\d+\s+(\d+)\.(\d\d)\s+([^\n]+)", info)
115                 cents = int(m.group(1))*100 + int(m.group(2))
116                 cokes.append('%i %i %s' % (i, cents, m.group(3)));
117
118         for c in cokes:
119                 c = c.strip()
120                 (slot_num, price, slot_name) = c.split(' ', 2)
121                 if slot_name == 'dead': continue
122                 choices += '%s-(%sc)-%s8 '%(slot_name, price, slot_num)
123
124 #       we don't want to print snacks for now since it'll be too large
125 #       and there's physical bits of paper in the machine anyway - matt
126 #       try:
127 #               snacks = get_snacks()
128 #       except:
129 #               snacks = {}
130 #
131 #       for slot, ( name, price ) in snacks.items():
132 #               choices += '%s8-%s (%sc) ' % ( slot, name, price )
133
134         choices += '55-DOOR '
135         choices += 'OR ANOTHER SNACK. '
136         choices += '99 TO READ AGAIN. '
137         choices += 'CHOICE?   '
138         msg.append((choices, False, None))
139         mk.set_messages(msg)
140
141 _pin_uid = 0
142 _pin_uname = 'root'
143 _pin_pin = '----'
144
145 def _check_pin(uid, pin):
146         global _pin_uid
147         global _pin_uname
148         global _pin_pin
149         print "_check_pin('",uid,"',---)"
150         if uid != _pin_uid:
151                 try:
152                         info = pwd.getpwuid(uid)
153                 except KeyError:
154                         logging.info('getting pin for uid %d: user not in password file'%uid)
155                         return None
156                 if info.pw_dir == None: return False
157                 pinfile = os.path.join(info.pw_dir, '.pin')
158                 try:
159                         s = os.stat(pinfile)
160                 except OSError:
161                         logging.info('getting pin for uid %d: .pin not found in home directory'%uid)
162                         return None
163                 if s.st_mode & 077:
164                         logging.info('getting pin for uid %d: .pin has wrong permissions. Fixing.'%uid)
165                         os.chmod(pinfile, 0600)
166                 try:
167                         f = file(pinfile)
168                 except IOError:
169                         logging.info('getting pin for uid %d: I cannot read pin file'%uid)
170                         return None
171                 pinstr = f.readline()
172                 f.close()
173                 if not re.search('^'+'[0-9]'*PIN_LENGTH+'$', pinstr):
174                         logging.info('getting pin for uid %d: %s not a good pin'%(uid,repr(pinstr)))
175                         return None
176                 _pin_uid = uid
177                 _pin_pin = pinstr
178                 _pin_uname = info.pw_name
179         else:
180                 pinstr = _pin_pin
181         if pin == int(pinstr):
182                 logging.info("Pin correct for %d",uid)
183         else:
184                 logging.info("Pin incorrect for %d",uid)
185         return pin == int(pinstr)
186
187 def acct_is_disabled(name=None):
188         global _pin_uname
189         if name == None:
190                 name = _pin_uname
191         acct, unused = Popen(['dispense', 'acct', _pin_uname], close_fds=True, stdout=PIPE).communicate()
192         # this is fucking appalling
193         flags = acct[acct.find("(")+1:acct.find(")")].strip()
194         if 'disabled' in flags:
195                 return True
196         if 'internal' in flags:
197                 return True
198         return False
199
200 def has_good_pin(uid):
201         return _check_pin(uid, None) != None
202
203 def verify_user_pin(uid, pin, skip_pin_check=False):
204         if skip_pin_check or _check_pin(uid, pin) == True:
205                 info = pwd.getpwuid(uid)
206                 if skip_pin_check:
207                         if acct_is_disabled(info.pw_name):
208                                 logging.info('refused mifare for disabled acct uid %d (%s)'%(uid,info.pw_name))
209                                 return '-disabled-'
210                         logging.info('accepted mifare for uid %d (%s)'%(uid,info.pw_name))
211                 else:
212                         logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
213                 return info.pw_name
214         else:
215                 logging.info('refused pin for uid %d'%(uid))
216                 return None
217
218
219 def cookie(v):
220         seed(time())
221         messages = ['  WASSUP! ', 'PINK FISH ', ' SECRETS ', '  ESKIMO  ', ' FORTUNES ', 'MORE MONEY']
222         choice = int(random()*len(messages))
223         msg = messages[choice]
224         left = range(len(msg))
225         for i in range(len(msg)):
226                 if msg[i] == ' ': left.remove(i)
227         reveal = 1
228         while left:
229                 s = ''
230                 for i in range(0, len(msg)):
231                         if i in left:
232                                 if reveal == 0:
233                                         left.remove(i)
234                                         s += msg[i]
235                                 else:
236                                         s += chr(int(random()*26)+ord('A'))
237                                 reveal += 1
238                                 reveal %= 17
239                         else:
240                                 s += msg[i]
241                 v.display(s)
242
243 def center(str):
244         LEN = 10
245         return ' '*((LEN-len(str))/2)+str
246
247
248
249 idlers = []
250 idler = None
251
252 def setup_idlers(v):
253         global idlers, idler
254         idlers = [
255                  GrayIdler(v),
256                 StringIdler(v, text="Kill 'em all", repeat=False),
257                  GrayIdler(v,one="*",zero="-"),
258                 StringIdler(v, text=CREDITS),
259                  GrayIdler(v,one="/",zero="\\"),
260                 ClockIdler(v),
261                  GrayIdler(v,one="X",zero="O"),
262                 FileIdler(v, '/usr/share/common-licenses/GPL-2',affinity=2),
263                  GrayIdler(v,one="*",zero="-",reorder=1),
264                 StringIdler(v, text=str(math.pi) + "            "),
265                 ClockIdler(v),
266                  GrayIdler(v,one="/",zero="\\",reorder=1),
267                 StringIdler(v, text=str(math.e) + "            "),
268                  GrayIdler(v,one="X",zero="O",reorder=1),
269                 StringIdler(v, text="    I want some pizza - please call Broadway Pizza on +61 8 9389 8500 - and order as Quinn - I am getting really hungry", repeat=False),
270                 PipeIdler(v, "/usr/bin/getent", "passwd"),
271                 FortuneIdler(v),
272                 ClockIdler(v),
273                 StringIdler(v),
274                 TrainIdler(v),
275                 # "Hello World" in brainfuck
276                 StringIdler(v, text=">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++."),
277                 ]
278         disabled = [
279                 ]
280
281 def reset_idler(v, vstatus, t = None):
282         global idlers, idler
283         idler = GreetingIdler(v, t)
284         vstatus.time_of_next_idlestep = time()+idler.next()
285         vstatus.time_of_next_idler = None
286         vstatus.time_to_autologout = None
287         vstatus.change_state(STATE_IDLE, 1)
288
289 def choose_idler():
290         global idlers, idler
291
292         # Implementation of the King Of the Hill algorithm from;
293         # http://eli.thegreenplace.net/2010/01/22/weighted-random-generation-in-python/
294
295         #def weighted_choice_king(weights):
296         #       total = 0
297         #       winner = 0
298         #       for i, w in enumerate(weights):
299         #               total += w
300         #               if random.random() * total < w:
301         #                       winner = i
302         #       return winner
303         #       
304
305         total = 0
306         winner = None
307         
308         for choice in idlers:
309                 weight = choice.affinity()
310                 total += weight
311                 if random() * total < weight:
312                         winner = choice
313
314         idler = winner
315
316         if idler:
317                 idler.reset()
318
319 def idle_step(vstatus):
320         global idler
321         if idler.finished():
322                 choose_idler()
323                 vstatus.time_of_next_idler = time() + 30
324         nextidle = idler.next()
325         if nextidle is None:
326                 nextidle = IDLE_SPEED
327         vstatus.time_of_next_idlestep = time()+nextidle
328
329 class VendState:
330         def __init__(self,v):
331                 self.state_table = {}
332                 self.state = STATE_IDLE
333                 self.counter = 0
334
335                 self.mk = MessageKeeper(v)
336                 self.cur_user = ''
337                 self.cur_pin = ''
338                 self.username = ''
339                 self.cur_selection = ''
340                 self.time_to_autologout = None
341
342                 self.last_timeout_refresh = None
343
344         def change_state(self,newstate,newcounter=None):
345                 if self.state != newstate:
346                         #print "Changing state from: ", 
347                         #print self.state,
348                         #print " to ", 
349                         #print newstate 
350                         self.state = newstate
351
352                 if newcounter is not None and self.counter != newcounter:
353                         #print "Changing counter from: ", 
354                         #print self.counter,
355                         #print " to ", 
356                         #print newcounter 
357                         self.counter = newcounter
358
359
360
361 def handle_tick_event(event, params, v, vstatus):
362         # don't care right now.
363         pass
364
365 def handle_switch_event(event, params, v, vstatus):
366         # don't care right now.
367         pass
368
369
370 def do_nothing(state, event, params, v, vstatus):
371         print "doing nothing (s,e,p)", state, " ", event, " ", params
372         pass
373
374 def handle_getting_uid_idle(state, event, params, v, vstatus):
375         # don't care right now.
376         pass
377
378 def handle_getting_pin_idle(state, event, params, v, vstatus):
379         # don't care right now.
380         pass
381
382 def handle_get_selection_idle(state, event, params, v, vstatus):
383         # don't care right now.
384         ###
385         ### State logging out ..
386         if vstatus.time_to_autologout != None:
387                 time_left = vstatus.time_to_autologout - time()
388                 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
389                         vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
390                         vstatus.last_timeout_refresh = int(time_left)
391                         vstatus.cur_selection = ''
392
393         if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
394                 vstatus.time_to_autologout = None
395                 vstatus.cur_user = ''
396                 vstatus.cur_pin = ''
397                 vstatus.cur_selection = ''
398                         
399                 reset_idler(v, vstatus)
400
401         ### State fully logged out ... reset variables
402         if vstatus.time_to_autologout and not vstatus.mk.done(): 
403                 vstatus.time_to_autologout = None
404         if vstatus.cur_user == '' and vstatus.time_to_autologout: 
405                 vstatus.time_to_autologout = None
406         
407         ### State logged in
408         if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
409                 # start autologout
410                 vstatus.time_to_autologout = time() + 15
411                 vstatus.last_timeout_refresh = None
412
413         ## FIXME - this may need to be elsewhere.....
414         # need to check
415         vstatus.mk.update_display()
416
417
418
419 def handle_get_selection_key(state, event, params, v, vstatus):
420         key = params
421         if len(vstatus.cur_selection) == 0:
422                 if key == 11:
423                         vstatus.cur_pin = ''
424                         vstatus.cur_user = ''
425                         vstatus.cur_selection = ''
426                         
427                         vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
428                         reset_idler(v, vstatus, 2)
429                         return
430                 vstatus.cur_selection += chr(key + ord('0'))
431                 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
432                 vstatus.time_to_autologout = None
433         elif len(vstatus.cur_selection) == 1:
434                 if key == 11:
435                         vstatus.cur_selection = ''
436                         vstatus.time_to_autologout = None
437                         scroll_options(vstatus.username, vstatus.mk)
438                         return
439                 else:
440                         vstatus.cur_selection += chr(key + ord('0'))
441                         if vstatus.cur_user:
442                                 make_selection(v,vstatus)
443                                 vstatus.cur_selection = ''
444                                 vstatus.time_to_autologout = time() + 8
445                                 vstatus.last_timeout_refresh = None
446                         else:
447                                 # Price check mode.
448                                 price_check(v,vstatus)
449                                 vstatus.cur_selection = ''
450                                 vstatus.time_to_autologout = None
451                                 vstatus.last_timeout_refresh = None
452
453 def make_selection(v, vstatus):
454         # should use sudo here
455         if vstatus.cur_selection == '55':
456                 vstatus.mk.set_message('OPENSESAME')
457                 logging.info('dispensing a door for %s'%vstatus.username)
458                 if geteuid() == 0:
459                         #ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
460                         ret = os.system('dispense -u "%s" door'%vstatus.username)
461                 else:
462                         ret = os.system('dispense door')
463                 if ret == 0:
464                         logging.info('door opened')
465                         vstatus.mk.set_message(center('DOOR OPEN'))
466                 else:
467                         logging.warning('user %s tried to dispense a bad door'%vstatus.username)
468                         vstatus.mk.set_message(center('BAD DOOR'))
469                 sleep(1)
470         elif vstatus.cur_selection == '81':
471                 cookie(v)
472         elif vstatus.cur_selection == '99':
473                 scroll_options(vstatus.username, vstatus.mk)
474                 vstatus.cur_selection = ''
475                 return
476         elif vstatus.cur_selection[1] == '8':
477                 v.display('GOT DRINK?')
478                 if ((os.system('dispense -u "%s" coke:%s'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
479                         v.display('SEEMS NOT')
480                 else:
481                         v.display('GOT DRINK!')
482         else:
483                 # first see if it's a named slot
484                 try:
485                         price, shortname, name = get_snack( vstatus.cur_selection )
486                 except:
487                         price, shortname, name = get_snack( '--' )
488                 dollarprice = "$%.2f" % ( price / 100.0 )
489                 v.display(vstatus.cur_selection+' - %s'%dollarprice)
490 #               exitcode = os.system('dispense -u "%s" give \>snacksales %d "%s"'%(vstatus.username, price, name)) >> 8
491 #               exitcode = os.system('dispense -u "%s" give \>sales\:snack %d "%s"'%(vstatus.username, price, name)) >> 8
492                 exitcode = os.system('dispense -u "%s" snack:%s'%(vstatus.username, vstatus.cur_selection)) >> 8
493                 if (exitcode == 0):
494                         # magic dispense syslog service
495                         syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
496                         (worked, code, string) = v.vend(vstatus.cur_selection)
497                         if worked:
498                                 v.display('THANK YOU')
499                         else:
500                                 print "Vend Failed:", code, string
501                                 v.display('VEND FAIL')
502                 elif (exitcode == 5):   # RV_BALANCE
503                         v.display('NO MONEY?')
504                 elif (exitcode == 4):   # RV_ARGUMENTS (zero give causes arguments)
505                         v.display('EMPTY SLOT')
506                 elif (exitcode == 1):   # RV_BADITEM (Dead slot)
507                         v.display('EMPTY SLOT')
508                 else:
509                         syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
510                         v.display('UNK ERROR')
511         sleep(1)
512
513
514 def price_check(v, vstatus):
515         if vstatus.cur_selection[1] == '8':
516                 args = ('dispense', 'iteminfo', 'coke:' + vstatus.cur_selection[0])
517                 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
518                 dollarprice = re.match("\s*[a-z]+:\d+\s+(\d+\.\d\d)\s+([^\n]+)", info).group(1)
519         else:
520                 # first see if it's a named slot
521                 try:
522                         price, shortname, name = get_snack( vstatus.cur_selection )
523                 except:
524                         price, shortname, name = get_snack( '--' )
525                 dollarprice = "$%.2f" % ( price / 100.0 )
526         v.display(vstatus.cur_selection+' - %s'%dollarprice)
527
528
529 def handle_getting_pin_key(state, event, params, v, vstatus):
530         #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
531         key = params
532         if len(vstatus.cur_pin) < PIN_LENGTH:
533                 if key == 11:
534                         if vstatus.cur_pin == '':
535                                 vstatus.cur_user = ''
536                                 reset_idler(v, vstatus)
537
538                                 return
539                         vstatus.cur_pin = ''
540                         vstatus.mk.set_message('PIN: ')
541                         return
542                 vstatus.cur_pin += chr(key + ord('0'))
543                 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
544                 if len(vstatus.cur_pin) == PIN_LENGTH:
545                         vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
546                         if vstatus.username:
547                                 v.beep(0, False)
548                                 vstatus.cur_selection = ''
549                                 vstatus.change_state(STATE_GET_SELECTION)
550                                 scroll_options(vstatus.username, vstatus.mk, True)
551                                 return
552                         else:
553                                 v.beep(40, False)
554                                 vstatus.mk.set_messages(
555                                         [(center('BAD PIN'), False, 1.0),
556                                          (center('SORRY'), False, 0.5)])
557                                 vstatus.cur_user = ''
558                                 vstatus.cur_pin = ''
559                         
560                                 reset_idler(v, vstatus, 2)
561
562                                 return
563
564
565 def handle_getting_uid_key(state, event, params, v, vstatus):
566         #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
567         key = params
568
569         # complicated key handling here:
570
571         if len(vstatus.cur_user) == 0 and key == 9:
572                 vstatus.cur_selection = ''
573                 vstatus.time_to_autologout = None
574                 vstatus.mk.set_message('PRICECHECK')
575                 sleep(0.5)
576                 scroll_options('', vstatus.mk)
577                 vstatus.change_state(STATE_GET_SELECTION)
578                 return
579
580         if len(vstatus.cur_user) <8:
581                 if key == 11:
582                         vstatus.cur_user = ''
583
584                         reset_idler(v, vstatus)
585                         return
586                 vstatus.cur_user += chr(key + ord('0'))
587                 #logging.info('dob: '+vstatus.cur_user)
588                 if len(vstatus.cur_user) > 5:
589                         vstatus.mk.set_message('>'+vstatus.cur_user)
590                 else:
591                         vstatus.mk.set_message('UID: '+vstatus.cur_user)
592         
593         if len(vstatus.cur_user) == 5:
594                 uid = int(vstatus.cur_user)
595
596                 if uid == 0:
597                         logging.info('user '+vstatus.cur_user+' has a bad PIN')
598                         pfalken="""
599 CARRIER DETECTED
600
601 CONNECT 128000
602
603 Welcome to Picklevision Sytems, Sunnyvale, CA
604
605 Greetings Professor Falken.
606
607
608
609
610 Shall we play a game?
611
612
613 Please choose from the following menu:
614
615 1. Tic-Tac-Toe
616 2. Chess
617 3. Checkers
618 4. Backgammon
619 5. Poker
620 6. Toxic and Biochemical Warfare
621 7. Global Thermonuclear War
622
623 7 [ENTER]
624
625 Wouldn't you prefer a nice game of chess?
626
627 """.replace('\n','    ')
628                         vstatus.mk.set_messages([(pfalken, False, 10)])
629                         vstatus.cur_user = ''
630                         vstatus.cur_pin = ''
631                         
632                         reset_idler(v, vstatus, 10)
633
634                         return
635
636                 if not has_good_pin(uid):
637                         logging.info('user '+vstatus.cur_user+' has a bad PIN')
638                         vstatus.mk.set_messages(
639                                 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
640                         vstatus.cur_user = ''
641                         vstatus.cur_pin = ''
642                         
643                         reset_idler(v, vstatus, 3)
644
645                         return
646                 
647                 if acct_is_disabled():
648                         logging.info('user '+vstatus.cur_user+' is disabled')
649                         vstatus.mk.set_messages(
650                                 [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
651                         vstatus.cur_user = ''
652                         vstatus.cur_pin = ''
653                         
654                         reset_idler(v, vstatus, 3)
655                         return
656
657
658                 vstatus.cur_pin = ''
659                 vstatus.mk.set_message('PIN: ')
660                 logging.info('need pin for user %s'%vstatus.cur_user)
661                 vstatus.change_state(STATE_GETTING_PIN)
662                 return
663
664
665 def handle_idle_key(state, event, params, v, vstatus):
666         #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
667
668         key = params
669
670         if key == 11:
671                 vstatus.cur_user = ''
672                 reset_idler(v, vstatus)
673                 return
674         
675         vstatus.change_state(STATE_GETTING_UID)
676         run_handler(event, key, v, vstatus)
677
678
679 def handle_idle_tick(state, event, params, v, vstatus):
680         ### State idling
681         if vstatus.mk.done():
682                 idle_step(vstatus)
683
684         if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
685                 vstatus.time_of_next_idler = time() + 30
686                 choose_idler()
687         
688         ###
689
690         vstatus.mk.update_display()
691
692         vstatus.change_state(STATE_GRANDFATHER_CLOCK)
693         run_handler(event, params, v, vstatus)
694         sleep(0.05)
695
696 def beep_on(when, before=0):
697         start = int(when - before)
698         end = int(when)
699         now = int(time())
700
701         if now >= start and now <= end:
702                 return 1
703         return 0
704
705 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
706         ### check for interesting times
707         now = localtime()
708
709         quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
710         halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
711         threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
712         fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
713
714         hourfromnow = localtime(time() + 3600)
715         
716         #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
717         onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
718                 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
719
720         ## check for X seconds to the hour
721         ## if case, update counter to 2
722         if beep_on(onthehour,15) \
723                 or beep_on(halfhour,0) \
724                 or beep_on(quarterhour,0) \
725                 or beep_on(threequarterhour,0) \
726                 or beep_on(fivetothehour,0):
727                 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
728                 run_handler(event, params, v, vstatus)
729         else:
730                 vstatus.change_state(STATE_IDLE)
731
732 def handle_grandfather_tick(state, event, params, v, vstatus):
733         go_idle = 1
734
735         msg = []
736         ### we live in interesting times
737         now = localtime()
738
739         quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
740         halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
741         threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
742         fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
743
744         hourfromnow = localtime(time() + 3600)
745         
746 #       onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
747         onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
748                 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
749
750
751         #print "when it fashionable to wear a onion on your hip"
752
753         if beep_on(onthehour,15):
754                 go_idle = 0
755                 next_hour=((hourfromnow[3] + 11) % 12) + 1
756                 if onthehour - time() < next_hour and onthehour - time() > 0:
757                         v.beep(0, False)
758
759                         t = int(time())
760                         if (t % 2) == 0:
761                                 msg.append(("DING!", False, None))
762                         else:
763                                 msg.append(("     DING!", False, None))
764                 elif int(onthehour - time()) == 0:
765                         v.beep(255, False)
766                         msg.append(("   BONG!", False, None))
767                         msg.append(("     IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
768         elif beep_on(halfhour,0):
769                 go_idle = 0
770                 v.beep(0, False)
771                 msg.append((" HALFHOUR ", False, 50))
772         elif beep_on(quarterhour,0):
773                 go_idle = 0
774                 v.beep(0, False)
775                 msg.append((" QTR HOUR ", False, 50))
776         elif beep_on(threequarterhour,0):
777                 go_idle = 0
778                 v.beep(0, False)
779                 msg.append((" 3 QTR HR ", False, 50))
780         elif beep_on(fivetothehour,0):
781                 go_idle = 0
782                 v.beep(0, False)
783                 msg.append(("Quick run to your lectures!  Hurry! Hurry!", False, TEXT_SPEED*4))
784         else:
785                 go_idle = 1
786         
787         ## check for X seconds to the hour
788
789         if len(msg):
790                 vstatus.mk.set_messages(msg)
791                 sleep(1)
792
793         vstatus.mk.update_display()
794         ## if no longer case, return to idle
795
796         ## change idler to be clock
797         if go_idle and vstatus.mk.done():
798                 vstatus.change_state(STATE_IDLE,1)
799
800 def handle_door_idle(state, event, params, v, vstatus):
801         def twiddle(clock,v,wise = 2):
802                 if (clock % 4 == 0):
803                         v.display("-FEED  ME-")
804                 elif (clock % 4 == 1+wise):
805                         v.display("\\FEED  ME/")
806                 elif (clock % 4 == 2):
807                         v.display("-FEED  ME-")
808                 elif (clock % 4 == 3-wise):
809                         v.display("/FEED  ME\\")
810
811         # don't care right now.
812         now = int(time())
813
814         if ((now % 60 % 2) == 0):
815                 twiddle(now, v)
816         else:
817                 twiddle(now, v, wise=0)
818
819
820 def handle_door_event(state, event, params, v, vstatus):
821         if params == 0:  #door open
822                 vstatus.change_state(STATE_DOOR_OPENING)
823                 logging.warning("Entering open door mode")
824                 v.display("-FEED  ME-")
825                 #door_open_mode(v);
826                 vstatus.cur_user = ''
827                 vstatus.cur_pin = ''
828         elif params == 1:  #door closed
829                 vstatus.change_state(STATE_DOOR_CLOSING)
830                 reset_idler(v, vstatus, 3)
831
832                 logging.warning('Leaving open door mode')
833                 v.display("-YUM YUM!-")
834
835 def handle_mifare_event(state, event, params, v, vstatus):
836         card_id = params
837         # Translate card_id into uid.
838         if card_id == None:
839                 return
840
841         try:
842                 vstatus.cur_user = get_uid(card_id)
843                 logging.info('Mapped card id to uid %s'%vstatus.cur_user)
844                 vstatus.username = get_uname(vstatus.cur_user)
845                 if acct_is_disabled(vstatus.username):
846                         vstatus.username = '-disabled-'
847         except ValueError:
848                 vstatus.username = None
849         if vstatus.username == '-disabled-':
850                 v.beep(40, False)
851                 vstatus.mk.set_messages(
852                         [(center('ACCT DISABLED'), False, 1.0),
853                          (center('SORRY'), False, 0.5)])
854                 vstatus.cur_user = ''
855                 vstatus.cur_pin = ''
856                 vstatus.username = None
857         
858                 reset_idler(v, vstatus, 2)
859                 return
860         elif vstatus.username:
861                 v.beep(0, False)
862                 vstatus.cur_selection = ''
863                 vstatus.change_state(STATE_GET_SELECTION)
864                 scroll_options(vstatus.username, vstatus.mk, True)
865                 return
866         else:
867                 v.beep(40, False)
868                 vstatus.mk.set_messages(
869                         [(center('BAD CARD'), False, 1.0),
870                          (center('SORRY'), False, 0.5)])
871                 vstatus.cur_user = ''
872                 vstatus.cur_pin = ''
873         
874                 reset_idler(v, vstatus, 2)
875                 return
876
877 def handle_mifare_add_user_event(state, event, params, v, vstatus):
878         card_id = params
879
880         # Translate card_id into uid.
881         if card_id == None:
882                 return
883
884         try:
885                 if get_uid(card_id) != None:
886                         vstatus.mk.set_messages(
887                                 [(center('ALREADY'), False, 0.5),
888                                  (center('ENROLLED'), False, 0.5)])
889
890                         # scroll_options(vstatus.username, vstatus.mk)
891                         return
892         except ValueError:
893                 pass
894
895         logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
896         set_card_id(vstatus.cur_user, card_id)
897         vstatus.mk.set_messages(
898                 [(center('CARD'), False, 0.5),
899                  (center('ENROLLED'), False, 0.5)])
900
901         # scroll_options(vstatus.username, vstatus.mk)
902
903 def return_to_idle(state,event,params,v,vstatus):
904         reset_idler(v, vstatus)
905
906 def create_state_table(vstatus):
907         vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
908         vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
909         vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
910         vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
911
912         vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
913         vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
914         vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
915         vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
916
917         vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
918         vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
919         vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
920         vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
921
922         vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
923         vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
924         vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
925         vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
926
927         vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
928         vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
929         vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
930         vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
931
932         vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
933         vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
934         vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
935         vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
936
937         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
938         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
939         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
940         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
941         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
942         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
943         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
944
945 def get_state_table_handler(vstatus, state, event, counter):
946         return vstatus.state_table[(state,event,counter)]
947
948 def time_to_next_update(vstatus):
949         idle_update = vstatus.time_of_next_idlestep - time()
950         if not vstatus.mk.done() and vstatus.mk.next_update is not None:
951                 mk_update = vstatus.mk.next_update - time()
952                 if mk_update < idle_update:
953                         idle_update = mk_update
954         return idle_update
955
956 def run_forever(rfh, wfh, options, cf):
957         v = VendingMachine(rfh, wfh, USE_MIFARE)
958         vstatus = VendState(v)
959         create_state_table(vstatus)
960
961         logging.debug('PING is ' + str(v.ping()))
962
963         if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
964
965         setup_idlers(v)
966         reset_idler(v, vstatus)
967
968         # This main loop was hideous and the work of the devil.
969         # This has now been fixed (mostly) - mtearle
970         #
971         #
972         # notes for later surgery
973         #   (event, counter, ' ')
974         #        V
975         #   d[      ] = (method)
976         #
977         # ( return state - not currently implemented )
978
979         while True:
980                 if USE_DB:
981                         try:
982                                 db.handle_events()
983                         except DispenseDatabaseException, e:
984                                 logging.error('Database error: '+str(e))
985
986                 timeout = time_to_next_update(vstatus)
987                 e = v.next_event(timeout)
988                 (event, params) = e
989
990                 run_handler(event, params, v, vstatus)
991
992 #               logging.debug('Got event: ' + repr(e))
993
994
995 def run_handler(event, params, v, vstatus):
996         handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
997         if handler:
998                 handler(vstatus.state, event, params, v, vstatus)
999
1000 def connect_to_vend(options, cf):
1001
1002         if options.use_lat:
1003                 logging.info('Connecting to vending machine using LAT')
1004                 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
1005                 rfh, wfh = latclient.get_fh()
1006         elif options.use_serial:
1007                 # Open vending machine via serial.
1008                 logging.info('Connecting to vending machine using serial')
1009                 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
1010                 rfh,wfh = serialclient.get_fh()
1011         else:
1012                 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
1013                 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
1014                 import socket
1015                 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
1016                 sock.connect((options.host, options.port))
1017                 rfh = sock.makefile('r')
1018                 wfh = sock.makefile('w')
1019                 global USE_MIFARE
1020                 USE_MIFARE = 0
1021                 
1022         return rfh, wfh
1023
1024 def parse_args():
1025         from optparse import OptionParser
1026
1027         op = OptionParser(usage="%prog [OPTION]...")
1028         op.add_option('-f', '--config-file', default='/etc/dispense2/servers.conf', metavar='FILE', dest='config_file', help='use the specified config file instead of /etc/dispense/servers.conf')
1029         op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
1030         op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
1031         op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
1032         op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
1033         op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
1034         op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
1035         op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
1036         op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
1037         op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
1038         op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
1039         op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
1040         options, args = op.parse_args()
1041
1042         if len(args) != 0:
1043                 op.error('extra command line arguments: ' + ' '.join(args))
1044
1045         return options
1046
1047 config_options = {
1048         'DBServer': ('Database', 'Server'),
1049         'DBName': ('Database', 'Name'),
1050         'DBUser': ('VendingMachine', 'DBUser'),
1051         'DBPassword': ('VendingMachine', 'DBPassword'),
1052         
1053         'ServiceName': ('VendingMachine', 'ServiceName'),
1054         'ServicePassword': ('VendingMachine', 'Password'),
1055         
1056         'ServerName': ('DecServer', 'Name'),
1057         'ConnectPassword': ('DecServer', 'ConnectPassword'),
1058         'PrivPassword': ('DecServer', 'PrivPassword'),
1059         }
1060
1061 class VendConfigFile:
1062         def __init__(self, config_file, options):
1063                 try:
1064                         cp = ConfigParser.ConfigParser()
1065                         cp.read(config_file)
1066
1067                         for option in options:
1068                                 section, name = options[option]
1069                                 value = cp.get(section, name)
1070                                 self.__dict__[option] = value
1071                 
1072                 except ConfigParser.Error, e:
1073                         raise SystemExit("Error reading config file "+config_file+": " + str(e))
1074
1075 def create_pid_file(name):
1076         try:
1077                 pid_file = file(name, 'w')
1078                 pid_file.write('%d\n'%os.getpid())
1079                 pid_file.close()
1080         except IOError, e:
1081                 logging.warning('unable to write to pid file '+name+': '+str(e))
1082
1083 def set_stuff_up():
1084         def do_nothing(signum, stack):
1085                 signal.signal(signum, do_nothing)
1086         def stop_server(signum, stack): raise KeyboardInterrupt
1087         signal.signal(signal.SIGHUP, do_nothing)
1088         signal.signal(signal.SIGTERM, stop_server)
1089         signal.signal(signal.SIGINT, stop_server)
1090
1091         options = parse_args()
1092         config_opts = VendConfigFile(options.config_file, config_options)
1093         if options.daemon: become_daemon()
1094         set_up_logging(options)
1095         if options.pid_file != '': create_pid_file(options.pid_file)
1096
1097         return options, config_opts
1098
1099 def clean_up_nicely(options, config_opts):
1100         if options.pid_file != '':
1101                 try:
1102                         os.unlink(options.pid_file)
1103                         logging.debug('Removed pid file '+options.pid_file)
1104                 except OSError: pass  # if we can't delete it, meh
1105
1106 def set_up_logging(options):
1107         logger = logging.getLogger()
1108         
1109         if not options.daemon:
1110                 stderr_logger = logging.StreamHandler(sys.stderr)
1111                 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1112                 logger.addHandler(stderr_logger)
1113         
1114         if options.log_file != '':
1115                 try:
1116                         file_logger = logging.FileHandler(options.log_file)
1117                         file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1118                         logger.addHandler(file_logger)
1119                 except IOError, e:
1120                         logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1121
1122         if options.syslog != None:
1123                 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1124                 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1125                 logger.addHandler(sys_logger)
1126
1127         if options.quiet:
1128                 logger.setLevel(logging.WARNING)
1129         elif options.verbose:
1130                 logger.setLevel(logging.DEBUG)
1131         else:
1132                 logger.setLevel(logging.INFO)
1133
1134 def become_daemon():
1135         dev_null = file('/dev/null')
1136         fd = dev_null.fileno()
1137         os.dup2(fd, 0)
1138         os.dup2(fd, 1)
1139         os.dup2(fd, 2)
1140         try:
1141                 if os.fork() != 0:
1142                         sys.exit(0)
1143                 os.setsid()
1144         except OSError, e:
1145                 raise SystemExit('failed to fork: '+str(e))
1146
1147 def do_vend_server(options, config_opts):
1148         while True:
1149                 try:
1150                         rfh, wfh = connect_to_vend(options, config_opts)
1151                 except (SerialClientException, socket.error), e:
1152                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
1153                         del exc_traceback
1154                         logging.error("Connection error: "+str(exc_type)+" "+str(e))
1155                         logging.info("Trying again in 5 seconds.")
1156                         sleep(5)
1157                         continue
1158                 
1159 #               run_forever(rfh, wfh, options, config_opts)
1160                 
1161                 try:
1162                         run_forever(rfh, wfh, options, config_opts)
1163                 except VendingException:
1164                         logging.error("Connection died, trying again...")
1165                         logging.info("Trying again in 5 seconds.")
1166                         sleep(5)
1167
1168 if __name__ == '__main__':
1169         options, config_opts = set_stuff_up()
1170         while True:
1171                 try:
1172                         logging.warning('Starting Vend Server')
1173                         do_vend_server(options, config_opts)
1174                         logging.error('Vend Server finished unexpectedly, restarting')
1175                 except KeyboardInterrupt:
1176                         logging.info("Killed by signal, cleaning up")
1177                         clean_up_nicely(options, config_opts)
1178                         logging.warning("Vend Server stopped")
1179                         break
1180                 except SystemExit:
1181                         break
1182                 except:
1183                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
1184                         tb = format_tb(exc_traceback, 20)
1185                         del exc_traceback
1186                         
1187                         logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1188                         logging.critical("Message: " + str(exc_value))
1189                         logging.critical("Traceback:")
1190                         for event in tb:
1191                                 for line in event.split('\n'):
1192                                         logging.critical('    '+line)
1193                         logging.critical("This message should be considered a bug in the Vend Server.")
1194                         logging.critical("Please report this to someone who can fix it.")
1195                         sleep(10)
1196                         logging.warning("Trying again anyway (might not help, but hey...)")
1197

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