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

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