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

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