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

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