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

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