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

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