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

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