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

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