Update mifare port. Add function to get uid.
[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         def twiddle(clock,v,wise = 2):
752                 if (clock % 4 == 0):
753                         v.display("-FEED  ME-")
754                 elif (clock % 4 == 1+wise):
755                         v.display("\\FEED  ME/")
756                 elif (clock % 4 == 2):
757                         v.display("-FEED  ME-")
758                 elif (clock % 4 == 3-wise):
759                         v.display("/FEED  ME\\")
760
761         # don't care right now.
762         now = int(time())
763
764         if ((now % 60 % 2) == 0):
765                 twiddle(now, v)
766         else:
767                 twiddle(now, v, wise=0)
768
769
770 def handle_door_event(state, event, params, v, vstatus):
771         if params == 0:  #door open
772                 vstatus.change_state(STATE_DOOR_OPENING)
773                 logging.warning("Entering open door mode")
774                 v.display("-FEED  ME-")
775                 #door_open_mode(v);
776                 vstatus.cur_user = ''
777                 vstatus.cur_pin = ''
778         elif params == 1:  #door closed
779                 vstatus.change_state(STATE_DOOR_CLOSING)
780                 reset_idler(v, vstatus, 3)
781
782                 logging.warning('Leaving open door mode')
783                 v.display("-YUM YUM!-")
784
785 def return_to_idle(state,event,params,v,vstatus):
786         reset_idler(v, vstatus)
787
788 def create_state_table(vstatus):
789         vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
790         vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
791         vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
792
793         vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
794         vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
795         vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
796
797         vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
798         vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
799         vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
800
801         vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
802         vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
803         vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
804
805         vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
806         vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
807         vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
808
809         vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
810         vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
811         vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
812
813         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
814         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
815         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
816         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
817         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
818         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
819
820 def get_state_table_handler(vstatus, state, event, counter):
821         return vstatus.state_table[(state,event,counter)]
822
823 def time_to_next_update(vstatus):
824         idle_update = vstatus.time_of_next_idlestep - time()
825         if not vstatus.mk.done() and vstatus.mk.next_update is not None:
826                 mk_update = vstatus.mk.next_update - time()
827                 if mk_update < idle_update:
828                         idle_update = mk_update
829         return idle_update
830
831 def run_forever(rfh, wfh, options, cf):
832         v = VendingMachine(rfh, wfh)
833         vstatus = VendState(v)
834         create_state_table(vstatus)
835
836         logging.debug('PING is ' + str(v.ping()))
837
838         if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
839
840         setup_idlers(v)
841         reset_idler(v, vstatus)
842
843         # This main loop was hideous and the work of the devil.
844         # This has now been fixed (mostly) - mtearle
845         #
846         #
847         # notes for later surgery
848         #   (event, counter, ' ')
849         #        V
850         #   d[      ] = (method)
851         #
852         # ( return state - not currently implemented )
853
854         while True:
855                 if USE_DB:
856                         try:
857                                 db.handle_events()
858                         except DispenseDatabaseException, e:
859                                 logging.error('Database error: '+str(e))
860
861
862                 timeout = time_to_next_update(vstatus)
863                 e = v.next_event(timeout)
864                 (event, params) = e
865
866                 run_handler(event, params, v, vstatus)
867
868 #               logging.debug('Got event: ' + repr(e))
869
870
871 def run_handler(event, params, v, vstatus):
872         handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
873         if handler:
874                 handler(vstatus.state, event, params, v, vstatus)
875
876 def connect_to_vend(options, cf):
877
878         if options.use_lat:
879                 logging.info('Connecting to vending machine using LAT')
880                 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
881                 rfh, wfh = latclient.get_fh()
882         elif options.use_serial:
883                 # Open vending machine via serial.
884                 logging.info('Connecting to vending machine using serial')
885                 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
886                 rfh,wfh = serialclient.get_fh()
887         else:
888                 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
889                 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
890                 import socket
891                 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
892                 sock.connect((options.host, options.port))
893                 rfh = sock.makefile('r')
894                 wfh = sock.makefile('w')
895                 
896         return rfh, wfh
897
898 def parse_args():
899         from optparse import OptionParser
900
901         op = OptionParser(usage="%prog [OPTION]...")
902         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')
903         op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
904         op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
905         op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
906         op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
907         op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
908         op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
909         op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
910         op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
911         op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
912         op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
913         op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
914         options, args = op.parse_args()
915
916         if len(args) != 0:
917                 op.error('extra command line arguments: ' + ' '.join(args))
918
919         return options
920
921 config_options = {
922         'DBServer': ('Database', 'Server'),
923         'DBName': ('Database', 'Name'),
924         'DBUser': ('VendingMachine', 'DBUser'),
925         'DBPassword': ('VendingMachine', 'DBPassword'),
926         
927         'ServiceName': ('VendingMachine', 'ServiceName'),
928         'ServicePassword': ('VendingMachine', 'Password'),
929         
930         'ServerName': ('DecServer', 'Name'),
931         'ConnectPassword': ('DecServer', 'ConnectPassword'),
932         'PrivPassword': ('DecServer', 'PrivPassword'),
933         }
934
935 class VendConfigFile:
936         def __init__(self, config_file, options):
937                 try:
938                         cp = ConfigParser.ConfigParser()
939                         cp.read(config_file)
940
941                         for option in options:
942                                 section, name = options[option]
943                                 value = cp.get(section, name)
944                                 self.__dict__[option] = value
945                 
946                 except ConfigParser.Error, e:
947                         raise SystemExit("Error reading config file "+config_file+": " + str(e))
948
949 def create_pid_file(name):
950         try:
951                 pid_file = file(name, 'w')
952                 pid_file.write('%d\n'%os.getpid())
953                 pid_file.close()
954         except IOError, e:
955                 logging.warning('unable to write to pid file '+name+': '+str(e))
956
957 def set_stuff_up():
958         def do_nothing(signum, stack):
959                 signal.signal(signum, do_nothing)
960         def stop_server(signum, stack): raise KeyboardInterrupt
961         signal.signal(signal.SIGHUP, do_nothing)
962         signal.signal(signal.SIGTERM, stop_server)
963         signal.signal(signal.SIGINT, stop_server)
964
965         options = parse_args()
966         config_opts = VendConfigFile(options.config_file, config_options)
967         if options.daemon: become_daemon()
968         set_up_logging(options)
969         if options.pid_file != '': create_pid_file(options.pid_file)
970
971         return options, config_opts
972
973 def clean_up_nicely(options, config_opts):
974         if options.pid_file != '':
975                 try:
976                         os.unlink(options.pid_file)
977                         logging.debug('Removed pid file '+options.pid_file)
978                 except OSError: pass  # if we can't delete it, meh
979
980 def set_up_logging(options):
981         logger = logging.getLogger()
982         
983         if not options.daemon:
984                 stderr_logger = logging.StreamHandler(sys.stderr)
985                 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
986                 logger.addHandler(stderr_logger)
987         
988         if options.log_file != '':
989                 try:
990                         file_logger = logging.FileHandler(options.log_file)
991                         file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
992                         logger.addHandler(file_logger)
993                 except IOError, e:
994                         logger.warning('unable to write to log file '+options.log_file+': '+str(e))
995
996         if options.syslog != None:
997                 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
998                 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
999                 logger.addHandler(sys_logger)
1000
1001         if options.quiet:
1002                 logger.setLevel(logging.WARNING)
1003         elif options.verbose:
1004                 logger.setLevel(logging.DEBUG)
1005         else:
1006                 logger.setLevel(logging.INFO)
1007
1008 def become_daemon():
1009         dev_null = file('/dev/null')
1010         fd = dev_null.fileno()
1011         os.dup2(fd, 0)
1012         os.dup2(fd, 1)
1013         os.dup2(fd, 2)
1014         try:
1015                 if os.fork() != 0:
1016                         sys.exit(0)
1017                 os.setsid()
1018         except OSError, e:
1019                 raise SystemExit('failed to fork: '+str(e))
1020
1021 def do_vend_server(options, config_opts):
1022         while True:
1023                 try:
1024                         rfh, wfh = connect_to_vend(options, config_opts)
1025                 except (SerialClientException, socket.error), e:
1026                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
1027                         del exc_traceback
1028                         logging.error("Connection error: "+str(exc_type)+" "+str(e))
1029                         logging.info("Trying again in 5 seconds.")
1030                         sleep(5)
1031                         continue
1032                 
1033                 try:
1034                         run_forever(rfh, wfh, options, config_opts)
1035                 except VendingException:
1036                         logging.error("Connection died, trying again...")
1037                         logging.info("Trying again in 5 seconds.")
1038                         sleep(5)
1039
1040 if __name__ == '__main__':
1041         options, config_opts = set_stuff_up()
1042         while True:
1043                 try:
1044                         logging.warning('Starting Vend Server')
1045                         do_vend_server(options, config_opts)
1046                         logging.error('Vend Server finished unexpectedly, restarting')
1047                 except KeyboardInterrupt:
1048                         logging.info("Killed by signal, cleaning up")
1049                         clean_up_nicely(options, config_opts)
1050                         logging.warning("Vend Server stopped")
1051                         break
1052                 except SystemExit:
1053                         break
1054                 except:
1055                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
1056                         tb = format_tb(exc_traceback, 20)
1057                         del exc_traceback
1058                         
1059                         logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1060                         logging.critical("Message: " + str(exc_value))
1061                         logging.critical("Traceback:")
1062                         for event in tb:
1063                                 for line in event.split('\n'):
1064                                         logging.critical('    '+line)
1065                         logging.critical("This message should be considered a bug in the Vend Server.")
1066                         logging.critical("Please report this to someone who can fix it.")
1067                         sleep(10)
1068                         logging.warning("Trying again anyway (might not help, but hey...)")
1069

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