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

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