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

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