Added addCard to DispenseInterface
[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                 _last_card_id = -1
891         
892                 reset_idler(v, vstatus, 2)
893                 return
894
895 def handle_mifare_add_user_event(state, event, params, v, vstatus):
896         global _last_card_id
897         card_id = params
898
899         # Translate card_id into uid.
900         if card_id == None or card_id == _last_card_id:
901                 return
902
903         _last_card_id = card_id
904         
905         try:
906                 if get_uid(card_id) != None:
907                         vstatus.mk.set_messages(
908                                 [(center('ALREADY'), False, 0.5),
909                                  (center('ENROLLED'), False, 0.5)])
910
911                         # scroll_options(vstatus.username, vstatus.mk)
912                         return
913         except ValueError:
914                 pass
915
916         logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
917         set_card_id(vstatus.cur_user, card_id)
918         vstatus.mk.set_messages(
919                 [(center('CARD'), False, 0.5),
920                  (center('ENROLLED'), False, 0.5)])
921
922         # scroll_options(vstatus.username, vstatus.mk)
923
924 def return_to_idle(state,event,params,v,vstatus):
925         reset_idler(v, vstatus)
926
927 def create_state_table(vstatus):
928         vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
929         vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
930         vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
931         vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
932
933         vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
934         vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
935         vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
936         vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
937
938         vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
939         vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
940         vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
941         vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
942
943         vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
944         vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = handle_door_event
945         vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
946         vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
947
948         vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
949         vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = handle_door_event
950         vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
951         vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
952
953         vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
954         vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = handle_door_event
955         vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
956         vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
957
958         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
959         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
960         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = handle_door_event
961         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = handle_door_event
962         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
963         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
964         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
965
966 def get_state_table_handler(vstatus, state, event, counter):
967         return vstatus.state_table[(state,event,counter)]
968
969 def time_to_next_update(vstatus):
970         idle_update = vstatus.time_of_next_idlestep - time()
971         if not vstatus.mk.done() and vstatus.mk.next_update is not None:
972                 mk_update = vstatus.mk.next_update - time()
973                 if mk_update < idle_update:
974                         idle_update = mk_update
975         return idle_update
976
977 def run_forever(rfh, wfh, options, cf):
978         v = VendingMachine(rfh, wfh, USE_MIFARE)
979         vstatus = VendState(v)
980         create_state_table(vstatus)
981
982         logging.debug('PING is ' + str(v.ping()))
983
984         if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
985
986         setup_idlers(v)
987         reset_idler(v, vstatus)
988
989         # This main loop was hideous and the work of the devil.
990         # This has now been fixed (mostly) - mtearle
991         #
992         #
993         # notes for later surgery
994         #   (event, counter, ' ')
995         #        V
996         #   d[      ] = (method)
997         #
998         # ( return state - not currently implemented )
999
1000         while True:
1001                 if USE_DB:
1002                         try:
1003                                 db.handle_events()
1004                         except DispenseDatabaseException, e:
1005                                 logging.error('Database error: '+str(e))
1006
1007                 timeout = time_to_next_update(vstatus)
1008                 e = v.next_event(timeout)
1009                 (event, params) = e
1010
1011                 run_handler(event, params, v, vstatus)
1012
1013 #               logging.debug('Got event: ' + repr(e))
1014
1015
1016 def run_handler(event, params, v, vstatus):
1017         handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
1018         if handler:
1019                 handler(vstatus.state, event, params, v, vstatus)
1020
1021 def connect_to_vend(options, cf):
1022
1023         if options.use_lat:
1024                 logging.info('Connecting to vending machine using LAT')
1025                 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
1026                 rfh, wfh = latclient.get_fh()
1027         elif options.use_serial:
1028                 # Open vending machine via serial.
1029                 logging.info('Connecting to vending machine using serial')
1030                 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
1031                 rfh,wfh = serialclient.get_fh()
1032         else:
1033                 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
1034                 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
1035                 import socket
1036                 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
1037                 sock.connect((options.host, options.port))
1038                 rfh = sock.makefile('r')
1039                 wfh = sock.makefile('w')
1040                 global USE_MIFARE
1041                 USE_MIFARE = 0
1042                 
1043         return rfh, wfh
1044
1045 def parse_args():
1046         from optparse import OptionParser
1047
1048         op = OptionParser(usage="%prog [OPTION]...")
1049         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')
1050         op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
1051         op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
1052         op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
1053         op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
1054         op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
1055         op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
1056         op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
1057         op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
1058         op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
1059         op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
1060         op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
1061         options, args = op.parse_args()
1062
1063         if len(args) != 0:
1064                 op.error('extra command line arguments: ' + ' '.join(args))
1065
1066         return options
1067
1068 config_options = {
1069         'DBServer': ('Database', 'Server'),
1070         'DBName': ('Database', 'Name'),
1071         'DBUser': ('VendingMachine', 'DBUser'),
1072         'DBPassword': ('VendingMachine', 'DBPassword'),
1073         
1074         'ServiceName': ('VendingMachine', 'ServiceName'),
1075         'ServicePassword': ('VendingMachine', 'Password'),
1076         
1077         'ServerName': ('DecServer', 'Name'),
1078         'ConnectPassword': ('DecServer', 'ConnectPassword'),
1079         'PrivPassword': ('DecServer', 'PrivPassword'),
1080         }
1081
1082 class VendConfigFile:
1083         def __init__(self, config_file, options):
1084                 try:
1085                         cp = ConfigParser.ConfigParser()
1086                         cp.read(config_file)
1087
1088                         for option in options:
1089                                 section, name = options[option]
1090                                 value = cp.get(section, name)
1091                                 self.__dict__[option] = value
1092                 
1093                 except ConfigParser.Error, e:
1094                         raise SystemExit("Error reading config file "+config_file+": " + str(e))
1095
1096 def create_pid_file(name):
1097         try:
1098                 pid_file = file(name, 'w')
1099                 pid_file.write('%d\n'%os.getpid())
1100                 pid_file.close()
1101         except IOError, e:
1102                 logging.warning('unable to write to pid file '+name+': '+str(e))
1103
1104 def set_stuff_up():
1105         def do_nothing(signum, stack):
1106                 signal.signal(signum, do_nothing)
1107         def stop_server(signum, stack): raise KeyboardInterrupt
1108         signal.signal(signal.SIGHUP, do_nothing)
1109         signal.signal(signal.SIGTERM, stop_server)
1110         signal.signal(signal.SIGINT, stop_server)
1111
1112         options = parse_args()
1113         config_opts = VendConfigFile(options.config_file, config_options)
1114         if options.daemon: become_daemon()
1115         set_up_logging(options)
1116         if options.pid_file != '': create_pid_file(options.pid_file)
1117
1118         return options, config_opts
1119
1120 def clean_up_nicely(options, config_opts):
1121         if options.pid_file != '':
1122                 try:
1123                         os.unlink(options.pid_file)
1124                         logging.debug('Removed pid file '+options.pid_file)
1125                 except OSError: pass  # if we can't delete it, meh
1126
1127 def set_up_logging(options):
1128         logger = logging.getLogger()
1129         
1130         if not options.daemon:
1131                 stderr_logger = logging.StreamHandler(sys.stderr)
1132                 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1133                 logger.addHandler(stderr_logger)
1134         
1135         if options.log_file != '':
1136                 try:
1137                         file_logger = logging.FileHandler(options.log_file)
1138                         file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1139                         logger.addHandler(file_logger)
1140                 except IOError, e:
1141                         logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1142
1143         if options.syslog != None:
1144                 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1145                 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1146                 logger.addHandler(sys_logger)
1147
1148         if options.quiet:
1149                 logger.setLevel(logging.WARNING)
1150         elif options.verbose:
1151                 logger.setLevel(logging.DEBUG)
1152         else:
1153                 logger.setLevel(logging.INFO)
1154
1155 def become_daemon():
1156         dev_null = file('/dev/null')
1157         fd = dev_null.fileno()
1158         os.dup2(fd, 0)
1159         os.dup2(fd, 1)
1160         os.dup2(fd, 2)
1161         try:
1162                 if os.fork() != 0:
1163                         sys.exit(0)
1164                 os.setsid()
1165         except OSError, e:
1166                 raise SystemExit('failed to fork: '+str(e))
1167
1168 def do_vend_server(options, config_opts):
1169         while True:
1170                 try:
1171                         rfh, wfh = connect_to_vend(options, config_opts)
1172                 except (SerialClientException, socket.error), e:
1173                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
1174                         del exc_traceback
1175                         logging.error("Connection error: "+str(exc_type)+" "+str(e))
1176                         logging.info("Trying again in 5 seconds.")
1177                         sleep(5)
1178                         continue
1179                 
1180 #               run_forever(rfh, wfh, options, config_opts)
1181                 
1182                 try:
1183                         run_forever(rfh, wfh, options, config_opts)
1184                 except VendingException:
1185                         logging.error("Connection died, trying again...")
1186                         logging.info("Trying again in 5 seconds.")
1187                         sleep(5)
1188
1189
1190 def main(argv=None):
1191         options, config_opts = set_stuff_up()
1192         while True:
1193                 try:
1194                         logging.warning('Starting Vend Server')
1195                         do_vend_server(options, config_opts)
1196                         logging.error('Vend Server finished unexpectedly, restarting')
1197                 except KeyboardInterrupt:
1198                         logging.info("Killed by signal, cleaning up")
1199                         clean_up_nicely(options, config_opts)
1200                         logging.warning("Vend Server stopped")
1201                         break
1202                 except SystemExit:
1203                         break
1204                 except:
1205                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
1206                         tb = format_tb(exc_traceback, 20)
1207                         del exc_traceback
1208                         
1209                         logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1210                         logging.critical("Message: " + str(exc_value))
1211                         logging.critical("Traceback:")
1212                         for event in tb:
1213                                 for line in event.split('\n'):
1214                                         logging.critical('    '+line)
1215                         logging.critical("This message should be considered a bug in the Vend Server.")
1216                         logging.critical("Please report this to someone who can fix it.")
1217                         sleep(10)
1218                         logging.warning("Trying again anyway (might not help, but hey...)")
1219
1220 if __name__ == '__main__':
1221         sys.exit(main())

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