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

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