0126063c4b5dc6b96ac43f6ecb98e513634765c5
[zanchey/dispense2.git] / sql-edition / servers / VendServer.py
1 #!/usr/bin/python
2 # vim:ts=4
3
4 USE_DB = 0
5 USE_MIFARE = 1
6
7 import ConfigParser
8 import sys, os, string, re, pwd, signal, math, syslog
9 import logging, logging.handlers
10 from traceback import format_tb
11 if USE_DB: import pg
12 from time import time, sleep, mktime, localtime
13 from subprocess import Popen, PIPE
14 from LATClient import LATClient, LATClientException
15 from SerialClient import SerialClient, SerialClientException
16 from VendingMachine import VendingMachine, VendingException
17 from MessageKeeper import MessageKeeper
18 from HorizScroll import HorizScroll
19 from random import random, seed
20 from Idler import GreetingIdler,TrainIdler,GrayIdler,StringIdler,ClockIdler,FortuneIdler,FileIdler,PipeIdler
21 from SnackConfig import get_snack#, get_snacks
22 import socket
23 from posix import geteuid
24 from LDAPConnector import get_uid, set_card_id
25
26 CREDITS="""
27 This vending machine software brought to you by:
28 Bernard Blackham
29 Mark Tearle
30 Nick Bannon
31 Cameron Patrick
32 and a collective of hungry alpacas.
33
34
35
36 For a good time call +61 8 6488 3901
37
38
39
40 """
41
42 PIN_LENGTH = 4
43
44 DOOR = 1
45 SWITCH = 2
46 KEY = 3
47 TICK = 4
48 MIFARE = 5
49
50
51 (
52 STATE_IDLE,
53 STATE_DOOR_OPENING,
54 STATE_DOOR_CLOSING,
55 STATE_GETTING_UID,
56 STATE_GETTING_PIN,
57 STATE_GET_SELECTION,
58 STATE_GRANDFATHER_CLOCK,
59 ) = range(1,8)
60
61 TEXT_SPEED = 0.8
62 IDLE_SPEED = 0.05
63
64 class DispenseDatabaseException(Exception): pass
65
66 class DispenseDatabase:
67         def __init__(self, vending_machine, host, name, user, password):
68                 self.vending_machine = vending_machine
69                 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
70                 self.db.query('LISTEN vend_requests')
71
72         def process_requests(self):
73                 logging.debug('database processing')
74                 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
75                 try:
76                         outstanding = self.db.query(query).getresult()
77                 except (pg.error,), db_err:
78                         raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
79                 for (id, slot) in outstanding:
80                         (worked, code, string) = self.vending_machine.vend(slot)
81                         logging.debug (str((worked, code, string)))
82                         if worked:
83                                 query = 'SELECT vend_success(%s)'%id
84                                 self.db.query(query).getresult()
85                         else:
86                                 query = 'SELECT vend_failed(%s)'%id
87                                 self.db.query(query).getresult()
88
89         def handle_events(self):
90                 notifier = self.db.getnotify()
91                 while notifier is not None:
92                         self.process_requests()
93                         notify = self.db.getnotify()
94
95 def scroll_options(username, mk, welcome = False):
96         if welcome:
97                 # Balance checking
98                 acct, unused = Popen(['dispense', 'acct', username], close_fds=True, stdout=PIPE).communicate()
99                 # this is fucking appalling
100                 balance = acct[acct.find("$")+1:acct.find("(")].strip()
101         
102                 msg = [(center('WELCOME'), False, TEXT_SPEED),
103                            (center(username), False, TEXT_SPEED),
104                            (center(balance), False, TEXT_SPEED),]
105         else:
106                 msg = []
107         choices = ' '*10+'CHOICES: '
108
109         # Get coke contents
110         cokes = []
111         for i in range(0, 7):
112                 args = ('dispense', 'iteminfo', 'coke:%i' % i)
113                 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
114                 m = re.match("\s*[a-z]+:\d+\s+(\d+)\.(\d\d)\s+([^\n]+)", info)
115                 cents = int(m.group(1))*100 + int(m.group(2))
116                 cokes.append('%i %i %s' % (i, cents, m.group(3)));
117
118         for c in cokes:
119                 c = c.strip()
120                 (slot_num, price, slot_name) = c.split(' ', 2)
121                 if slot_name == 'dead': continue
122                 choices += '%s-(%sc)-%s8 '%(slot_name, price, slot_num)
123
124 #       we don't want to print snacks for now since it'll be too large
125 #       and there's physical bits of paper in the machine anyway - matt
126 #       try:
127 #               snacks = get_snacks()
128 #       except:
129 #               snacks = {}
130 #
131 #       for slot, ( name, price ) in snacks.items():
132 #               choices += '%s8-%s (%sc) ' % ( slot, name, price )
133
134         choices += '55-DOOR '
135         choices += 'OR ANOTHER SNACK. '
136         choices += '99 TO READ AGAIN. '
137         choices += 'CHOICE?   '
138         msg.append((choices, False, None))
139         mk.set_messages(msg)
140
141 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                         ret = os.system('dispense -u "%s" door'%vstatus.username)
415                 else:
416                         ret = os.system('dispense door')
417                 if ret == 0:
418                         logging.info('door opened')
419                         vstatus.mk.set_message(center('DOOR OPEN'))
420                 else:
421                         logging.warning('user %s tried to dispense a bad door'%vstatus.username)
422                         vstatus.mk.set_message(center('BAD DOOR'))
423                 sleep(1)
424         elif vstatus.cur_selection == '81':
425                 cookie(v)
426         elif vstatus.cur_selection == '99':
427                 scroll_options(vstatus.username, vstatus.mk)
428                 vstatus.cur_selection = ''
429                 return
430         elif vstatus.cur_selection[1] == '8':
431                 v.display('GOT DRINK?')
432                 if ((os.system('dispense -u "%s" coke:%s'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
433                         v.display('SEEMS NOT')
434                 else:
435                         v.display('GOT DRINK!')
436         else:
437                 # first see if it's a named slot
438                 try:
439                         price, shortname, name = get_snack( vstatus.cur_selection )
440                 except:
441                         price, shortname, name = get_snack( '--' )
442                 dollarprice = "$%.2f" % ( price / 100.0 )
443                 v.display(vstatus.cur_selection+' - %s'%dollarprice)
444                 exitcode = os.system('dispense -u "%s" give \>snacksales %d "%s"'%(vstatus.username, price, name)) >> 8
445 #               exitcode = os.system('dispense -u "%s" give \>sales\:snack %d "%s"'%(vstatus.username, price, name)) >> 8
446 #               exitcode = os.system('dispense -u "%s" snack:%s'%(vstatus.username, vstatus.cur_selection)) >> 8
447                 if (exitcode == 0):
448                         # magic dispense syslog service
449                         syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
450                         v.vend(vstatus.cur_selection)
451                         v.display('THANK YOU')
452                 elif (exitcode == 5):   # RV_BALANCE
453                         v.display('NO MONEY?')
454                 elif (exitcode == 4):   # RV_ARGUMENTS (zero give causes arguments)
455                         v.display('EMPTY SLOT')
456                 elif (exitcode == 1):   # RV_BADITEM (Dead slot)
457                         v.display('EMPTY SLOT')
458                 else:
459                         syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
460                         v.display('UNK ERROR')
461         sleep(1)
462
463
464 def price_check(v, vstatus):
465         if vstatus.cur_selection[1] == '8':
466                 args = ('dispense', 'iteminfo', 'coke:' + vstatus.cur_selection[0])
467                 info, unused = Popen(args, close_fds=True, stdout=PIPE).communicate()
468                 dollarprice = re.match("\s*[a-z]+:\d+\s+(\d+\.\d\d)\s+([^\n]+)", info).group(1)
469         else:
470                 # first see if it's a named slot
471                 try:
472                         price, shortname, name = get_snack( vstatus.cur_selection )
473                 except:
474                         price, shortname, name = get_snack( '--' )
475                 dollarprice = "$%.2f" % ( price / 100.0 )
476         v.display(vstatus.cur_selection+' - %s'%dollarprice)
477
478
479 def handle_getting_pin_key(state, event, params, v, vstatus):
480         #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
481         key = params
482         if len(vstatus.cur_pin) < PIN_LENGTH:
483                 if key == 11:
484                         if vstatus.cur_pin == '':
485                                 vstatus.cur_user = ''
486                                 reset_idler(v, vstatus)
487
488                                 return
489                         vstatus.cur_pin = ''
490                         vstatus.mk.set_message('PIN: ')
491                         return
492                 vstatus.cur_pin += chr(key + ord('0'))
493                 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
494                 if len(vstatus.cur_pin) == PIN_LENGTH:
495                         vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
496                         if vstatus.username:
497                                 v.beep(0, False)
498                                 vstatus.cur_selection = ''
499                                 vstatus.change_state(STATE_GET_SELECTION)
500                                 scroll_options(vstatus.username, vstatus.mk, True)
501                                 return
502                         else:
503                                 v.beep(40, False)
504                                 vstatus.mk.set_messages(
505                                         [(center('BAD PIN'), False, 1.0),
506                                          (center('SORRY'), False, 0.5)])
507                                 vstatus.cur_user = ''
508                                 vstatus.cur_pin = ''
509                         
510                                 reset_idler(v, vstatus, 2)
511
512                                 return
513
514
515 def handle_getting_uid_key(state, event, params, v, vstatus):
516         #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
517         key = params
518
519         # complicated key handling here:
520
521         if len(vstatus.cur_user) == 0 and key == 9:
522                 vstatus.cur_selection = ''
523                 vstatus.time_to_autologout = None
524                 vstatus.mk.set_message('PRICECHECK')
525                 sleep(0.5)
526                 scroll_options('', vstatus.mk)
527                 vstatus.change_state(STATE_GET_SELECTION)
528                 return
529
530         if len(vstatus.cur_user) <8:
531                 if key == 11:
532                         vstatus.cur_user = ''
533
534                         reset_idler(v, vstatus)
535                         return
536                 vstatus.cur_user += chr(key + ord('0'))
537                 #logging.info('dob: '+vstatus.cur_user)
538                 if len(vstatus.cur_user) > 5:
539                         vstatus.mk.set_message('>'+vstatus.cur_user)
540                 else:
541                         vstatus.mk.set_message('UID: '+vstatus.cur_user)
542         
543         if len(vstatus.cur_user) == 5:
544                 uid = int(vstatus.cur_user)
545
546                 if uid == 0:
547                         logging.info('user '+vstatus.cur_user+' has a bad PIN')
548                         pfalken="""
549 CARRIER DETECTED
550
551 CONNECT 128000
552
553 Welcome to Picklevision Sytems, Sunnyvale, CA
554
555 Greetings Professor Falken.
556
557
558
559
560 Shall we play a game?
561
562
563 Please choose from the following menu:
564
565 1. Tic-Tac-Toe
566 2. Chess
567 3. Checkers
568 4. Backgammon
569 5. Poker
570 6. Toxic and Biochemical Warfare
571 7. Global Thermonuclear War
572
573 7 [ENTER]
574
575 Wouldn't you prefer a nice game of chess?
576
577 """.replace('\n','    ')
578                         vstatus.mk.set_messages([(pfalken, False, 10)])
579                         vstatus.cur_user = ''
580                         vstatus.cur_pin = ''
581                         
582                         reset_idler(v, vstatus, 10)
583
584                         return
585
586                 if not has_good_pin(uid):
587                         logging.info('user '+vstatus.cur_user+' has a bad PIN')
588                         vstatus.mk.set_messages(
589                                 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
590                         vstatus.cur_user = ''
591                         vstatus.cur_pin = ''
592                         
593                         reset_idler(v, vstatus, 3)
594
595                         return
596
597
598                 vstatus.cur_pin = ''
599                 vstatus.mk.set_message('PIN: ')
600                 logging.info('need pin for user %s'%vstatus.cur_user)
601                 vstatus.change_state(STATE_GETTING_PIN)
602                 return
603
604
605 def handle_idle_key(state, event, params, v, vstatus):
606         #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
607
608         key = params
609
610         if key == 11:
611                 vstatus.cur_user = ''
612                 reset_idler(v, vstatus)
613                 return
614         
615         vstatus.change_state(STATE_GETTING_UID)
616         run_handler(event, key, v, vstatus)
617
618
619 def handle_idle_tick(state, event, params, v, vstatus):
620         ### State idling
621         if vstatus.mk.done():
622                 idle_step(vstatus)
623
624         if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
625                 vstatus.time_of_next_idler = time() + 30
626                 choose_idler()
627         
628         ###
629
630         vstatus.mk.update_display()
631
632         vstatus.change_state(STATE_GRANDFATHER_CLOCK)
633         run_handler(event, params, v, vstatus)
634         sleep(0.05)
635
636 def beep_on(when, before=0):
637         start = int(when - before)
638         end = int(when)
639         now = int(time())
640
641         if now >= start and now <= end:
642                 return 1
643         return 0
644
645 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
646         ### check for interesting times
647         now = localtime()
648
649         quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
650         halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
651         threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
652         fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
653
654         hourfromnow = localtime(time() + 3600)
655         
656         #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
657         onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
658                 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
659
660         ## check for X seconds to the hour
661         ## if case, update counter to 2
662         if beep_on(onthehour,15) \
663                 or beep_on(halfhour,0) \
664                 or beep_on(quarterhour,0) \
665                 or beep_on(threequarterhour,0) \
666                 or beep_on(fivetothehour,0):
667                 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
668                 run_handler(event, params, v, vstatus)
669         else:
670                 vstatus.change_state(STATE_IDLE)
671
672 def handle_grandfather_tick(state, event, params, v, vstatus):
673         go_idle = 1
674
675         msg = []
676         ### we live in interesting times
677         now = localtime()
678
679         quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
680         halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
681         threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
682         fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
683
684         hourfromnow = localtime(time() + 3600)
685         
686 #       onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
687         onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
688                 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
689
690
691         #print "when it fashionable to wear a onion on your hip"
692
693         if beep_on(onthehour,15):
694                 go_idle = 0
695                 next_hour=((hourfromnow[3] + 11) % 12) + 1
696                 if onthehour - time() < next_hour and onthehour - time() > 0:
697                         v.beep(0, False)
698
699                         t = int(time())
700                         if (t % 2) == 0:
701                                 msg.append(("DING!", False, None))
702                         else:
703                                 msg.append(("     DING!", False, None))
704                 elif int(onthehour - time()) == 0:
705                         v.beep(255, False)
706                         msg.append(("   BONG!", False, None))
707                         msg.append(("     IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
708         elif beep_on(halfhour,0):
709                 go_idle = 0
710                 v.beep(0, False)
711                 msg.append((" HALFHOUR ", False, 50))
712         elif beep_on(quarterhour,0):
713                 go_idle = 0
714                 v.beep(0, False)
715                 msg.append((" QTR HOUR ", False, 50))
716         elif beep_on(threequarterhour,0):
717                 go_idle = 0
718                 v.beep(0, False)
719                 msg.append((" 3 QTR HR ", False, 50))
720         elif beep_on(fivetothehour,0):
721                 go_idle = 0
722                 v.beep(0, False)
723                 msg.append(("Quick run to your lectures!  Hurry! Hurry!", False, TEXT_SPEED*4))
724         else:
725                 go_idle = 1
726         
727         ## check for X seconds to the hour
728
729         if len(msg):
730                 vstatus.mk.set_messages(msg)
731                 sleep(1)
732
733         vstatus.mk.update_display()
734         ## if no longer case, return to idle
735
736         ## change idler to be clock
737         if go_idle and vstatus.mk.done():
738                 vstatus.change_state(STATE_IDLE,1)
739
740 def handle_door_idle(state, event, params, v, vstatus):
741         def twiddle(clock,v,wise = 2):
742                 if (clock % 4 == 0):
743                         v.display("-FEED  ME-")
744                 elif (clock % 4 == 1+wise):
745                         v.display("\\FEED  ME/")
746                 elif (clock % 4 == 2):
747                         v.display("-FEED  ME-")
748                 elif (clock % 4 == 3-wise):
749                         v.display("/FEED  ME\\")
750
751         # don't care right now.
752         now = int(time())
753
754         if ((now % 60 % 2) == 0):
755                 twiddle(now, v)
756         else:
757                 twiddle(now, v, wise=0)
758
759
760 def handle_door_event(state, event, params, v, vstatus):
761         if params == 0:  #door open
762                 vstatus.change_state(STATE_DOOR_OPENING)
763                 logging.warning("Entering open door mode")
764                 v.display("-FEED  ME-")
765                 #door_open_mode(v);
766                 vstatus.cur_user = ''
767                 vstatus.cur_pin = ''
768         elif params == 1:  #door closed
769                 vstatus.change_state(STATE_DOOR_CLOSING)
770                 reset_idler(v, vstatus, 3)
771
772                 logging.warning('Leaving open door mode')
773                 v.display("-YUM YUM!-")
774
775 def handle_mifare_event(state, event, params, v, vstatus):
776         card_id = params
777         # Translate card_id into uid.
778         if card_id == None:
779                 return
780
781         try:
782                 vstatus.cur_user = get_uid(card_id)
783                 logging.info('Mapped card id to uid %s'%vstatus.cur_user)
784                 vstatus.username = verify_user_pin(int(vstatus.cur_user), None, True)
785         except ValueError:
786                 vstatus.username = None
787         if vstatus.username:
788                 v.beep(0, False)
789                 vstatus.cur_selection = ''
790                 vstatus.change_state(STATE_GET_SELECTION)
791                 scroll_options(vstatus.username, vstatus.mk, True)
792                 return
793         else:
794                 v.beep(40, False)
795                 vstatus.mk.set_messages(
796                         [(center('BAD CARD'), False, 1.0),
797                          (center('SORRY'), False, 0.5)])
798                 vstatus.cur_user = ''
799                 vstatus.cur_pin = ''
800         
801                 reset_idler(v, vstatus, 2)
802                 return
803
804 def handle_mifare_add_user_event(state, event, params, v, vstatus):
805         card_id = params
806
807         # Translate card_id into uid.
808         if card_id == None:
809                 return
810
811         try:
812                 if get_uid(card_id) != None:
813                         vstatus.mk.set_messages(
814                                 [(center('ALREADY'), False, 0.5),
815                                  (center('ENROLLED'), False, 0.5)])
816
817                         # scroll_options(vstatus.username, vstatus.mk)
818                         return
819         except ValueError:
820                 pass
821
822         logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
823         set_card_id(vstatus.cur_user, card_id)
824         vstatus.mk.set_messages(
825                 [(center('CARD'), False, 0.5),
826                  (center('ENROLLED'), False, 0.5)])
827
828         # scroll_options(vstatus.username, vstatus.mk)
829
830 def return_to_idle(state,event,params,v,vstatus):
831         reset_idler(v, vstatus)
832
833 def create_state_table(vstatus):
834         vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
835         vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
836         vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
837         vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
838
839         vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
840         vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
841         vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
842         vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
843
844         vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
845         vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
846         vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
847         vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
848
849         vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
850         vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
851         vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
852         vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
853
854         vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
855         vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
856         vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
857         vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
858
859         vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
860         vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
861         vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
862         vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
863
864         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
865         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
866         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
867         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
868         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
869         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
870         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
871
872 def get_state_table_handler(vstatus, state, event, counter):
873         return vstatus.state_table[(state,event,counter)]
874
875 def time_to_next_update(vstatus):
876         idle_update = vstatus.time_of_next_idlestep - time()
877         if not vstatus.mk.done() and vstatus.mk.next_update is not None:
878                 mk_update = vstatus.mk.next_update - time()
879                 if mk_update < idle_update:
880                         idle_update = mk_update
881         return idle_update
882
883 def run_forever(rfh, wfh, options, cf):
884         v = VendingMachine(rfh, wfh, USE_MIFARE)
885         vstatus = VendState(v)
886         create_state_table(vstatus)
887
888         logging.debug('PING is ' + str(v.ping()))
889
890         if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
891
892         setup_idlers(v)
893         reset_idler(v, vstatus)
894
895         # This main loop was hideous and the work of the devil.
896         # This has now been fixed (mostly) - mtearle
897         #
898         #
899         # notes for later surgery
900         #   (event, counter, ' ')
901         #        V
902         #   d[      ] = (method)
903         #
904         # ( return state - not currently implemented )
905
906         while True:
907                 if USE_DB:
908                         try:
909                                 db.handle_events()
910                         except DispenseDatabaseException, e:
911                                 logging.error('Database error: '+str(e))
912
913                 timeout = time_to_next_update(vstatus)
914                 e = v.next_event(timeout)
915                 (event, params) = e
916
917                 run_handler(event, params, v, vstatus)
918
919 #               logging.debug('Got event: ' + repr(e))
920
921
922 def run_handler(event, params, v, vstatus):
923         handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
924         if handler:
925                 handler(vstatus.state, event, params, v, vstatus)
926
927 def connect_to_vend(options, cf):
928
929         if options.use_lat:
930                 logging.info('Connecting to vending machine using LAT')
931                 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
932                 rfh, wfh = latclient.get_fh()
933         elif options.use_serial:
934                 # Open vending machine via serial.
935                 logging.info('Connecting to vending machine using serial')
936                 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
937                 rfh,wfh = serialclient.get_fh()
938         else:
939                 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
940                 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
941                 import socket
942                 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
943                 sock.connect((options.host, options.port))
944                 rfh = sock.makefile('r')
945                 wfh = sock.makefile('w')
946                 global USE_MIFARE
947                 USE_MIFARE = 0
948                 
949         return rfh, wfh
950
951 def parse_args():
952         from optparse import OptionParser
953
954         op = OptionParser(usage="%prog [OPTION]...")
955         op.add_option('-f', '--config-file', default='/etc/dispense2/servers.conf', metavar='FILE', dest='config_file', help='use the specified config file instead of /etc/dispense/servers.conf')
956         op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
957         op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
958         op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
959         op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
960         op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
961         op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
962         op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
963         op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
964         op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
965         op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
966         op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
967         options, args = op.parse_args()
968
969         if len(args) != 0:
970                 op.error('extra command line arguments: ' + ' '.join(args))
971
972         return options
973
974 config_options = {
975         'DBServer': ('Database', 'Server'),
976         'DBName': ('Database', 'Name'),
977         'DBUser': ('VendingMachine', 'DBUser'),
978         'DBPassword': ('VendingMachine', 'DBPassword'),
979         
980         'ServiceName': ('VendingMachine', 'ServiceName'),
981         'ServicePassword': ('VendingMachine', 'Password'),
982         
983         'ServerName': ('DecServer', 'Name'),
984         'ConnectPassword': ('DecServer', 'ConnectPassword'),
985         'PrivPassword': ('DecServer', 'PrivPassword'),
986         }
987
988 class VendConfigFile:
989         def __init__(self, config_file, options):
990                 try:
991                         cp = ConfigParser.ConfigParser()
992                         cp.read(config_file)
993
994                         for option in options:
995                                 section, name = options[option]
996                                 value = cp.get(section, name)
997                                 self.__dict__[option] = value
998                 
999                 except ConfigParser.Error, e:
1000                         raise SystemExit("Error reading config file "+config_file+": " + str(e))
1001
1002 def create_pid_file(name):
1003         try:
1004                 pid_file = file(name, 'w')
1005                 pid_file.write('%d\n'%os.getpid())
1006                 pid_file.close()
1007         except IOError, e:
1008                 logging.warning('unable to write to pid file '+name+': '+str(e))
1009
1010 def set_stuff_up():
1011         def do_nothing(signum, stack):
1012                 signal.signal(signum, do_nothing)
1013         def stop_server(signum, stack): raise KeyboardInterrupt
1014         signal.signal(signal.SIGHUP, do_nothing)
1015         signal.signal(signal.SIGTERM, stop_server)
1016         signal.signal(signal.SIGINT, stop_server)
1017
1018         options = parse_args()
1019         config_opts = VendConfigFile(options.config_file, config_options)
1020         if options.daemon: become_daemon()
1021         set_up_logging(options)
1022         if options.pid_file != '': create_pid_file(options.pid_file)
1023
1024         return options, config_opts
1025
1026 def clean_up_nicely(options, config_opts):
1027         if options.pid_file != '':
1028                 try:
1029                         os.unlink(options.pid_file)
1030                         logging.debug('Removed pid file '+options.pid_file)
1031                 except OSError: pass  # if we can't delete it, meh
1032
1033 def set_up_logging(options):
1034         logger = logging.getLogger()
1035         
1036         if not options.daemon:
1037                 stderr_logger = logging.StreamHandler(sys.stderr)
1038                 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1039                 logger.addHandler(stderr_logger)
1040         
1041         if options.log_file != '':
1042                 try:
1043                         file_logger = logging.FileHandler(options.log_file)
1044                         file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1045                         logger.addHandler(file_logger)
1046                 except IOError, e:
1047                         logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1048
1049         if options.syslog != None:
1050                 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1051                 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1052                 logger.addHandler(sys_logger)
1053
1054         if options.quiet:
1055                 logger.setLevel(logging.WARNING)
1056         elif options.verbose:
1057                 logger.setLevel(logging.DEBUG)
1058         else:
1059                 logger.setLevel(logging.INFO)
1060
1061 def become_daemon():
1062         dev_null = file('/dev/null')
1063         fd = dev_null.fileno()
1064         os.dup2(fd, 0)
1065         os.dup2(fd, 1)
1066         os.dup2(fd, 2)
1067         try:
1068                 if os.fork() != 0:
1069                         sys.exit(0)
1070                 os.setsid()
1071         except OSError, e:
1072                 raise SystemExit('failed to fork: '+str(e))
1073
1074 def do_vend_server(options, config_opts):
1075         while True:
1076                 try:
1077                         rfh, wfh = connect_to_vend(options, config_opts)
1078                 except (SerialClientException, socket.error), e:
1079                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
1080                         del exc_traceback
1081                         logging.error("Connection error: "+str(exc_type)+" "+str(e))
1082                         logging.info("Trying again in 5 seconds.")
1083                         sleep(5)
1084                         continue
1085                 
1086 #               run_forever(rfh, wfh, options, config_opts)
1087                 
1088                 try:
1089                         run_forever(rfh, wfh, options, config_opts)
1090                 except VendingException:
1091                         logging.error("Connection died, trying again...")
1092                         logging.info("Trying again in 5 seconds.")
1093                         sleep(5)
1094
1095 if __name__ == '__main__':
1096         options, config_opts = set_stuff_up()
1097         while True:
1098                 try:
1099                         logging.warning('Starting Vend Server')
1100                         do_vend_server(options, config_opts)
1101                         logging.error('Vend Server finished unexpectedly, restarting')
1102                 except KeyboardInterrupt:
1103                         logging.info("Killed by signal, cleaning up")
1104                         clean_up_nicely(options, config_opts)
1105                         logging.warning("Vend Server stopped")
1106                         break
1107                 except SystemExit:
1108                         break
1109                 except:
1110                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
1111                         tb = format_tb(exc_traceback, 20)
1112                         del exc_traceback
1113                         
1114                         logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1115                         logging.critical("Message: " + str(exc_value))
1116                         logging.critical("Traceback:")
1117                         for event in tb:
1118                                 for line in event.split('\n'):
1119                                         logging.critical('    '+line)
1120                         logging.critical("This message should be considered a bug in the Vend Server.")
1121                         logging.critical("Please report this to someone who can fix it.")
1122                         sleep(10)
1123                         logging.warning("Trying again anyway (might not help, but hey...)")
1124

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