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

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