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

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