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

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