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

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