0b45a40b03a9e36acd3d2ee086561f5511bad6dc
[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, syslog
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 DRINK?')
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 DRINK!')
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                 exitcode = os.system('su - "%s" -c "dispense give oday %d"'%(vstatus.username, price))
420                 if (exitcode == 0):
421                         # magic dispense syslog service
422                         syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
423                         v.vend(vstatus.cur_selection)
424                         v.display('THANK YOU')
425                 else:
426                         syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s, code %d" % (name, vstatus.cur_selection, vstatus.username, exitcode))
427                         v.display('NO MONEY?')
428         sleep(1)
429
430
431 def handle_getting_pin_key(state, event, params, v, vstatus):
432         #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
433         key = params
434         if len(vstatus.cur_pin) < PIN_LENGTH:
435                 if key == 11:
436                         if vstatus.cur_pin == '':
437                                 vstatus.cur_user = ''
438                                 reset_idler(v, vstatus)
439
440                                 return
441                         vstatus.cur_pin = ''
442                         vstatus.mk.set_message('PIN: ')
443                         return
444                 vstatus.cur_pin += chr(key + ord('0'))
445                 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
446                 if len(vstatus.cur_pin) == PIN_LENGTH:
447                         vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
448                         if vstatus.username:
449                                 v.beep(0, False)
450                                 vstatus.cur_selection = ''
451                                 vstatus.change_state(STATE_GET_SELECTION)
452                                 scroll_options(vstatus.username, vstatus.mk, True)
453                                 return
454                         else:
455                                 v.beep(40, False)
456                                 vstatus.mk.set_messages(
457                                         [(center('BAD PIN'), False, 1.0),
458                                          (center('SORRY'), False, 0.5)])
459                                 vstatus.cur_user = ''
460                                 vstatus.cur_pin = ''
461                         
462                                 reset_idler(v, vstatus, 2)
463
464                                 return
465
466
467 def handle_getting_uid_key(state, event, params, v, vstatus):
468         #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
469         key = params
470         # complicated key handling here:
471         if len(vstatus.cur_user) < 5:
472                 if key == 11:
473                         vstatus.cur_user = ''
474
475                         reset_idler(v, vstatus)
476                         return
477
478                 vstatus.cur_user += chr(key + ord('0'))
479                 vstatus.mk.set_message('UID: '+vstatus.cur_user)
480
481         if len(vstatus.cur_user) == 5:
482                 uid = int(vstatus.cur_user)
483                 if uid == 0:
484                         logging.info('user '+vstatus.cur_user+' has a bad PIN')
485                         pfalken="""
486 CARRIER DETECTED
487
488 CONNECT 128000
489
490 Welcome to Picklevision Sytems, Sunnyvale, CA
491
492 Greetings Professor Falken.
493
494
495
496
497 Shall we play a game?
498
499
500 Please choose from the following menu:
501
502 1. Tic-Tac-Toe
503 2. Chess
504 3. Checkers
505 4. Backgammon
506 5. Poker
507 6. Toxic and Biochemical Warfare
508 7. Global Thermonuclear War
509
510 7 [ENTER]
511
512 Wouldn't you prefer a nice game of chess?
513
514 """.replace('\n','    ')
515                         vstatus.mk.set_messages([(pfalken, False, 10)])
516                         vstatus.cur_user = ''
517                         vstatus.cur_pin = ''
518                         
519                         reset_idler(v, vstatus, 10)
520
521                         return
522
523                 if not has_good_pin(uid):
524                         logging.info('user '+vstatus.cur_user+' has a bad PIN')
525                         vstatus.mk.set_messages(
526                                 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
527                         vstatus.cur_user = ''
528                         vstatus.cur_pin = ''
529                         
530                         reset_idler(v, vstatus, 3)
531
532                         return
533
534
535                 vstatus.cur_pin = ''
536                 vstatus.mk.set_message('PIN: ')
537                 logging.info('need pin for user %s'%vstatus.cur_user)
538                 vstatus.change_state(STATE_GETTING_PIN)
539                 return
540
541
542 def handle_idle_key(state, event, params, v, vstatus):
543         #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
544
545         key = params
546
547         if key == 11:
548                 vstatus.cur_user = ''
549                 reset_idler(v, vstatus)
550                 return
551         
552         vstatus.change_state(STATE_GETTING_UID)
553         run_handler(event, key, v, vstatus)
554
555
556 def handle_idle_tick(state, event, params, v, vstatus):
557         ### State idling
558         if vstatus.mk.done():
559                 idle_step(vstatus)
560
561         if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
562                 vstatus.time_of_next_idler = time() + 30
563                 choose_idler()
564         
565         ###
566
567         vstatus.mk.update_display()
568
569         vstatus.change_state(STATE_GRANDFATHER_CLOCK)
570         run_handler(event, params, v, vstatus)
571         sleep(0.05)
572
573 def beep_on(when, before=0):
574         start = int(when - before)
575         end = int(when)
576         now = int(time())
577
578         if now >= start and now <= end:
579                 return 1
580         return 0
581
582 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
583         ### check for interesting times
584         now = localtime()
585
586         quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
587         halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
588         threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
589         fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
590
591         hourfromnow = localtime(time() + 3600)
592         
593         #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
594         onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
595                 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
596
597         ## check for X seconds to the hour
598         ## if case, update counter to 2
599         if beep_on(onthehour,15) \
600                 or beep_on(halfhour,0) \
601                 or beep_on(quarterhour,0) \
602                 or beep_on(threequarterhour,0) \
603                 or beep_on(fivetothehour,0):
604                 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
605                 run_handler(event, params, v, vstatus)
606         else:
607                 vstatus.change_state(STATE_IDLE)
608
609 def handle_grandfather_tick(state, event, params, v, vstatus):
610         go_idle = 1
611
612         msg = []
613         ### we live in interesting times
614         now = localtime()
615
616         quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
617         halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
618         threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
619         fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
620
621         hourfromnow = localtime(time() + 3600)
622         
623 #       onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
624         onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
625                 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
626
627
628         #print "when it fashionable to wear a onion on your hip"
629
630         if beep_on(onthehour,15):
631                 go_idle = 0
632                 next_hour=((hourfromnow[3] + 11) % 12) + 1
633                 if onthehour - time() < next_hour and onthehour - time() > 0:
634                         v.beep(0, False)
635
636                         t = int(time())
637                         if (t % 2) == 0:
638                                 msg.append(("DING!", False, None))
639                         else:
640                                 msg.append(("     DING!", False, None))
641                 elif int(onthehour - time()) == 0:
642                         v.beep(255, False)
643                         msg.append(("   BONG!", False, None))
644                         msg.append(("     IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
645         elif beep_on(halfhour,0):
646                 go_idle = 0
647                 v.beep(0, False)
648                 msg.append((" HALFHOUR ", False, 50))
649         elif beep_on(quarterhour,0):
650                 go_idle = 0
651                 v.beep(0, False)
652                 msg.append((" QTR HOUR ", False, 50))
653         elif beep_on(threequarterhour,0):
654                 go_idle = 0
655                 v.beep(0, False)
656                 msg.append((" 3 QTR HR ", False, 50))
657         elif beep_on(fivetothehour,0):
658                 go_idle = 0
659                 v.beep(0, False)
660                 msg.append(("Quick run to your lectures!  Hurry! Hurry!", False, TEXT_SPEED*4))
661         else:
662                 go_idle = 1
663         
664         ## check for X seconds to the hour
665
666         if len(msg):
667                 vstatus.mk.set_messages(msg)
668                 sleep(1)
669
670         vstatus.mk.update_display()
671         ## if no longer case, return to idle
672
673         ## change idler to be clock
674         if go_idle and vstatus.mk.done():
675                 vstatus.change_state(STATE_IDLE,1)
676
677 def handle_door_idle(state, event, params, v, vstatus):
678         # don't care right now.
679         pass
680
681 def handle_door_event(state, event, params, v, vstatus):
682         if params == 1:  #door open
683                 vstatus.change_state(STATE_DOOR_OPENING)
684                 logging.warning("Entering open door mode")
685                 v.display("-FEED  ME-")
686                 #door_open_mode(v);
687                 vstatus.cur_user = ''
688                 vstatus.cur_pin = ''
689         elif params == 0:  #door closed
690                 vstatus.change_state(STATE_DOOR_CLOSING)
691                 reset_idler(v, vstatus, 3)
692
693                 logging.warning('Leaving open door mode')
694                 v.display("-YUM YUM!-")
695
696 def return_to_idle(state,event,params,v,vstatus):
697         reset_idler(v, vstatus)
698
699 def create_state_table(vstatus):
700         vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
701         vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
702         vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
703
704         vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
705         vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
706         vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
707
708         vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
709         vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
710         vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
711
712         vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
713         vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
714         vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
715
716         vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
717         vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
718         vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
719
720         vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
721         vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
722         vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
723
724         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
725         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
726         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
727         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
728         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
729         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
730
731 def get_state_table_handler(vstatus, state, event, counter):
732         return vstatus.state_table[(state,event,counter)]
733
734 def time_to_next_update(vstatus):
735         idle_update = vstatus.time_of_next_idlestep - time()
736         if not vstatus.mk.done() and vstatus.mk.next_update is not None:
737                 mk_update = vstatus.mk.next_update - time()
738                 if mk_update < idle_update:
739                         idle_update = mk_update
740         return idle_update
741
742 def run_forever(rfh, wfh, options, cf):
743         v = VendingMachine(rfh, wfh)
744         vstatus = VendState(v)
745         create_state_table(vstatus)
746
747         logging.debug('PING is ' + str(v.ping()))
748
749         if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
750
751         setup_idlers(v)
752         reset_idler(v, vstatus)
753
754         # This main loop was hideous and the work of the devil.
755         # This has now been fixed (mostly) - mtearle
756         #
757         #
758         # notes for later surgery
759         #   (event, counter, ' ')
760         #        V
761         #   d[      ] = (method)
762         #
763         # ( return state - not currently implemented )
764
765         while True:
766                 if USE_DB:
767                         try:
768                                 db.handle_events()
769                         except DispenseDatabaseException, e:
770                                 logging.error('Database error: '+str(e))
771
772
773                 timeout = time_to_next_update(vstatus)
774                 e = v.next_event(timeout)
775                 (event, params) = e
776
777                 run_handler(event, params, v, vstatus)
778
779 #               logging.debug('Got event: ' + repr(e))
780
781
782 def run_handler(event, params, v, vstatus):
783         handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
784         if handler:
785                 handler(vstatus.state, event, params, v, vstatus)
786
787 def connect_to_vend(options, cf):
788
789         if options.use_lat:
790                 logging.info('Connecting to vending machine using LAT')
791                 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
792                 rfh, wfh = latclient.get_fh()
793         elif options.use_serial:
794                 # Open vending machine via serial.
795                 logging.info('Connecting to vending machine using serial')
796                 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
797                 rfh,wfh = serialclient.get_fh()
798         else:
799                 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
800                 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
801                 import socket
802                 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
803                 sock.connect((options.host, options.port))
804                 rfh = sock.makefile('r')
805                 wfh = sock.makefile('w')
806                 
807         return rfh, wfh
808
809 def parse_args():
810         from optparse import OptionParser
811
812         op = OptionParser(usage="%prog [OPTION]...")
813         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')
814         op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
815         op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
816         op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
817         op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
818         op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
819         op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
820         op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
821         op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
822         op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
823         op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
824         op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
825         options, args = op.parse_args()
826
827         if len(args) != 0:
828                 op.error('extra command line arguments: ' + ' '.join(args))
829
830         return options
831
832 config_options = {
833         'DBServer': ('Database', 'Server'),
834         'DBName': ('Database', 'Name'),
835         'DBUser': ('VendingMachine', 'DBUser'),
836         'DBPassword': ('VendingMachine', 'DBPassword'),
837         
838         'ServiceName': ('VendingMachine', 'ServiceName'),
839         'ServicePassword': ('VendingMachine', 'Password'),
840         
841         'ServerName': ('DecServer', 'Name'),
842         'ConnectPassword': ('DecServer', 'ConnectPassword'),
843         'PrivPassword': ('DecServer', 'PrivPassword'),
844         }
845
846 class VendConfigFile:
847         def __init__(self, config_file, options):
848                 try:
849                         cp = ConfigParser.ConfigParser()
850                         cp.read(config_file)
851
852                         for option in options:
853                                 section, name = options[option]
854                                 value = cp.get(section, name)
855                                 self.__dict__[option] = value
856                 
857                 except ConfigParser.Error, e:
858                         raise SystemExit("Error reading config file "+config_file+": " + str(e))
859
860 def create_pid_file(name):
861         try:
862                 pid_file = file(name, 'w')
863                 pid_file.write('%d\n'%os.getpid())
864                 pid_file.close()
865         except IOError, e:
866                 logging.warning('unable to write to pid file '+name+': '+str(e))
867
868 def set_stuff_up():
869         def do_nothing(signum, stack):
870                 signal.signal(signum, do_nothing)
871         def stop_server(signum, stack): raise KeyboardInterrupt
872         signal.signal(signal.SIGHUP, do_nothing)
873         signal.signal(signal.SIGTERM, stop_server)
874         signal.signal(signal.SIGINT, stop_server)
875
876         options = parse_args()
877         config_opts = VendConfigFile(options.config_file, config_options)
878         if options.daemon: become_daemon()
879         set_up_logging(options)
880         if options.pid_file != '': create_pid_file(options.pid_file)
881
882         return options, config_opts
883
884 def clean_up_nicely(options, config_opts):
885         if options.pid_file != '':
886                 try:
887                         os.unlink(options.pid_file)
888                         logging.debug('Removed pid file '+options.pid_file)
889                 except OSError: pass  # if we can't delete it, meh
890
891 def set_up_logging(options):
892         logger = logging.getLogger()
893         
894         if not options.daemon:
895                 stderr_logger = logging.StreamHandler(sys.stderr)
896                 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
897                 logger.addHandler(stderr_logger)
898         
899         if options.log_file != '':
900                 try:
901                         file_logger = logging.FileHandler(options.log_file)
902                         file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
903                         logger.addHandler(file_logger)
904                 except IOError, e:
905                         logger.warning('unable to write to log file '+options.log_file+': '+str(e))
906
907         if options.syslog != None:
908                 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
909                 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
910                 logger.addHandler(sys_logger)
911
912         if options.quiet:
913                 logger.setLevel(logging.WARNING)
914         elif options.verbose:
915                 logger.setLevel(logging.DEBUG)
916         else:
917                 logger.setLevel(logging.INFO)
918
919 def become_daemon():
920         dev_null = file('/dev/null')
921         fd = dev_null.fileno()
922         os.dup2(fd, 0)
923         os.dup2(fd, 1)
924         os.dup2(fd, 2)
925         try:
926                 if os.fork() != 0:
927                         sys.exit(0)
928                 os.setsid()
929         except OSError, e:
930                 raise SystemExit('failed to fork: '+str(e))
931
932 def do_vend_server(options, config_opts):
933         while True:
934                 try:
935                         rfh, wfh = connect_to_vend(options, config_opts)
936                 except (SerialClientException, socket.error), e:
937                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
938                         del exc_traceback
939                         logging.error("Connection error: "+str(exc_type)+" "+str(e))
940                         logging.info("Trying again in 5 seconds.")
941                         sleep(5)
942                         continue
943                 
944                 try:
945                         run_forever(rfh, wfh, options, config_opts)
946                 except VendingException:
947                         logging.error("Connection died, trying again...")
948                         logging.info("Trying again in 5 seconds.")
949                         sleep(5)
950
951 if __name__ == '__main__':
952         options, config_opts = set_stuff_up()
953         while True:
954                 try:
955                         logging.warning('Starting Vend Server')
956                         do_vend_server(options, config_opts)
957                         logging.error('Vend Server finished unexpectedly, restarting')
958                 except KeyboardInterrupt:
959                         logging.info("Killed by signal, cleaning up")
960                         clean_up_nicely(options, config_opts)
961                         logging.warning("Vend Server stopped")
962                         break
963                 except SystemExit:
964                         break
965                 except:
966                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
967                         tb = format_tb(exc_traceback, 20)
968                         del exc_traceback
969                         
970                         logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
971                         logging.critical("Message: " + str(exc_value))
972                         logging.critical("Traceback:")
973                         for event in tb:
974                                 for line in event.split('\n'):
975                                         logging.critical('    '+line)
976                         logging.critical("This message should be considered a bug in the Vend Server.")
977                         logging.critical("Please report this to someone who can fix it.")
978                         sleep(10)
979                         logging.warning("Trying again anyway (might not help, but hey...)")
980

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