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

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