[TPG] Increased serial timeout to account for vend time
[uccvend-vendserver.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_acct_state(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 'invalid'
152         ret = os.system('dispense acct %s' % (info.pw_name))
153         if ret != 0:
154                 return 'invalid'
155
156         # TODO: Disabled account check (done in server pin check now)   
157
158         return 'good'
159
160 def verify_user_pin(uid, pin, skip_pin_check=False):
161         info = pwd.getpwuid(uid)
162         if skip_pin_check:
163                 logging.info('accepted mifare for uid %d (%s)'%(uid,info.pw_name))
164         elif os.system('dispense pincheck %04i %s' % (pin, info.pw_name)) != 0:
165                 logging.info('refused pin for uid %d'%(uid))
166                 return None
167         else:
168                 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
169         return info.pw_name
170
171
172 def cookie(v):
173         seed(time())
174         messages = ['  WASSUP! ', 'PINK FISH ', ' SECRETS ', '  ESKIMO  ', ' FORTUNES ', 'MORE MONEY']
175         choice = int(random()*len(messages))
176         msg = messages[choice]
177         left = range(len(msg))
178         for i in range(len(msg)):
179                 if msg[i] == ' ': left.remove(i)
180         reveal = 1
181         while left:
182                 s = ''
183                 for i in range(0, len(msg)):
184                         if i in left:
185                                 if reveal == 0:
186                                         left.remove(i)
187                                         s += msg[i]
188                                 else:
189                                         s += chr(int(random()*26)+ord('A'))
190                                 reveal += 1
191                                 reveal %= 17
192                         else:
193                                 s += msg[i]
194                 v.display(s)
195
196 def center(str):
197         LEN = 10
198         return ' '*((LEN-len(str))/2)+str
199
200
201
202 idlers = []
203 idler = None
204
205 def setup_idlers(v):
206         global idlers, idler
207         idlers = [
208                  GrayIdler(v),
209                 StringIdler(v, text="Kill 'em all", repeat=False),
210                  GrayIdler(v,one="*",zero="-"),
211                 StringIdler(v, text=CREDITS),
212                  GrayIdler(v,one="/",zero="\\"),
213                 ClockIdler(v),
214                  GrayIdler(v,one="X",zero="O"),
215                 FileIdler(v, '/usr/share/common-licenses/GPL-2',affinity=2),
216                  GrayIdler(v,one="*",zero="-",reorder=1),
217                 StringIdler(v, text=str(math.pi) + "            "),
218                 ClockIdler(v),
219                  GrayIdler(v,one="/",zero="\\",reorder=1),
220                 StringIdler(v, text=str(math.e) + "            "),
221                  GrayIdler(v,one="X",zero="O",reorder=1),
222                 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),
223                 PipeIdler(v, "/usr/bin/getent", "passwd"),
224                 FortuneIdler(v),
225                 ClockIdler(v),
226                 StringIdler(v),
227                 TrainIdler(v),
228                 ]
229         disabled = [
230                 ]
231
232 def reset_idler(v, vstatus, t = None):
233         global idlers, idler
234         idler = GreetingIdler(v, t)
235         vstatus.time_of_next_idlestep = time()+idler.next()
236         vstatus.time_of_next_idler = None
237         vstatus.time_to_autologout = None
238         vstatus.change_state(STATE_IDLE, 1)
239
240 def choose_idler():
241         global idlers, idler
242         iiindex = 0
243         average_affinity = 10 # guessing here...
244
245         if idler and idler.__class__ != GreetingIdler:
246                 iiindex = idlers.index(idler)
247
248         iilen = len(idlers)
249
250         move = int(random()*len(idlers)*average_affinity) + 1
251
252         while move >= 0:
253                 iiindex += 1
254                 iiindex %= iilen
255                 idler = idlers[iiindex]
256                 move -= idler.affinity()
257
258         idler.reset()
259
260 def idle_step(vstatus):
261         global idler
262         if idler.finished():
263                 choose_idler()
264                 vstatus.time_of_next_idler = time() + 30
265         nextidle = idler.next()
266         if nextidle is None:
267                 nextidle = IDLE_SPEED
268         vstatus.time_of_next_idlestep = time()+nextidle
269
270 class VendState:
271         def __init__(self,v):
272                 self.state_table = {}
273                 self.state = STATE_IDLE
274                 self.counter = 0
275
276                 self.mk = MessageKeeper(v)
277                 self.cur_user = ''
278                 self.cur_pin = ''
279                 self.username = ''
280                 self.cur_selection = ''
281                 self.time_to_autologout = None
282
283                 self.last_timeout_refresh = None
284
285         def change_state(self,newstate,newcounter=None):
286                 if self.state != newstate:
287                         #print "Changing state from: ", 
288                         #print self.state,
289                         #print " to ", 
290                         #print newstate 
291                         self.state = newstate
292
293                 if newcounter is not None and self.counter != newcounter:
294                         #print "Changing counter from: ", 
295                         #print self.counter,
296                         #print " to ", 
297                         #print newcounter 
298                         self.counter = newcounter
299
300
301
302 def handle_tick_event(event, params, v, vstatus):
303         # don't care right now.
304         pass
305
306 def handle_switch_event(event, params, v, vstatus):
307         # don't care right now.
308         pass
309
310
311 def do_nothing(state, event, params, v, vstatus):
312         print "doing nothing (s,e,p)", state, " ", event, " ", params
313         pass
314
315 def handle_getting_uid_idle(state, event, params, v, vstatus):
316         # don't care right now.
317         pass
318
319 def handle_getting_pin_idle(state, event, params, v, vstatus):
320         # don't care right now.
321         pass
322
323 def handle_get_selection_idle(state, event, params, v, vstatus):
324         # don't care right now.
325         ###
326         ### State logging out ..
327         if vstatus.time_to_autologout != None:
328                 time_left = vstatus.time_to_autologout - time()
329                 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
330                         vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
331                         vstatus.last_timeout_refresh = int(time_left)
332                         vstatus.cur_selection = ''
333
334         if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
335                 vstatus.time_to_autologout = None
336                 vstatus.cur_user = ''
337                 vstatus.cur_pin = ''
338                 vstatus.cur_selection = ''
339                         
340                 reset_idler(v, vstatus)
341
342         ### State fully logged out ... reset variables
343         if vstatus.time_to_autologout and not vstatus.mk.done(): 
344                 vstatus.time_to_autologout = None
345         if vstatus.cur_user == '' and vstatus.time_to_autologout: 
346                 vstatus.time_to_autologout = None
347         
348         ### State logged in
349         if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
350                 # start autologout
351                 vstatus.time_to_autologout = time() + 15
352                 vstatus.last_timeout_refresh = None
353
354         ## FIXME - this may need to be elsewhere.....
355         # need to check
356         vstatus.mk.update_display()
357
358
359
360 def handle_get_selection_key(state, event, params, v, vstatus):
361         key = params
362         if len(vstatus.cur_selection) == 0:
363                 if key == 11:
364                         vstatus.cur_pin = ''
365                         vstatus.cur_user = ''
366                         vstatus.cur_selection = ''
367                         
368                         vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
369                         reset_idler(v, vstatus, 2)
370                         return
371                 vstatus.cur_selection += chr(key + ord('0'))
372                 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
373                 vstatus.time_to_autologout = None
374         elif len(vstatus.cur_selection) == 1:
375                 if key == 11:
376                         vstatus.cur_selection = ''
377                         vstatus.time_to_autologout = None
378                         scroll_options(vstatus.username, vstatus.mk)
379                         return
380                 else:
381                         vstatus.cur_selection += chr(key + ord('0'))
382                         if vstatus.cur_user:
383                                 make_selection(v,vstatus)
384                                 vstatus.cur_selection = ''
385                                 vstatus.time_to_autologout = time() + 8
386                                 vstatus.last_timeout_refresh = None
387                         else:
388                                 # Price check mode.
389                                 price_check(v,vstatus)
390                                 vstatus.cur_selection = ''
391                                 vstatus.time_to_autologout = None
392                                 vstatus.last_timeout_refresh = None
393
394 def make_selection(v, vstatus):
395         # should use sudo here
396         if vstatus.cur_selection == '55':
397                 vstatus.mk.set_message('OPENSESAME')
398                 logging.info('dispensing a door for %s'%vstatus.username)
399                 if geteuid() == 0:
400                         #ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
401                         ret = os.system('dispense -u "%s" door'%vstatus.username)
402                 else:
403                         ret = os.system('dispense door')
404                 if ret == 0:
405                         logging.info('door opened')
406                         vstatus.mk.set_message(center('DOOR OPEN'))
407                 else:
408                         logging.warning('user %s tried to dispense a bad door'%vstatus.username)
409                         vstatus.mk.set_message(center('BAD DOOR'))
410                 sleep(1)
411         elif vstatus.cur_selection == '81':
412                 cookie(v)
413         elif vstatus.cur_selection == '99':
414                 scroll_options(vstatus.username, vstatus.mk)
415                 vstatus.cur_selection = ''
416                 return
417         elif vstatus.cur_selection[1] == '8':
418                 v.display('GOT DRINK?')
419                 if ((os.system('dispense -u "%s" coke:%s'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
420                         v.display('SEEMS NOT')
421                 else:
422                         v.display('GOT DRINK!')
423                         #v.display('SEE FRIDGE')
424         else:
425                 # first see if it's a named slot
426                 try:
427                         price, shortname, name = get_snack( vstatus.cur_selection )
428                 except:
429                         price, shortname, name = get_snack( '--' )
430                 dollarprice = "$%.2f" % ( price / 100.0 )
431                 v.display(vstatus.cur_selection+' - %s'%dollarprice)
432 #               exitcode = os.system('dispense -u "%s" give \>sales\:snack %d "%s"'%(vstatus.username, price, name)) >> 8
433                 # For some reason, this causes the machine and this code to desync
434                 exitcode = os.system('dispense -u "%s" snack:%s'%(vstatus.username, vstatus.cur_selection)) >> 8
435                 if (exitcode == 0):
436                         # magic dispense syslog service
437                         syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
438                         v.vend(vstatus.cur_selection)
439                         v.display('THANK YOU')
440                 elif (exitcode == 5):   # RV_BALANCE
441                         v.display('NO MONEY?')
442                 elif (exitcode == 4):   # RV_ARGUMENTS (zero give causes arguments)
443                         v.display('EMPTY SLOT')
444                 elif (exitcode == 1):   # RV_BADITEM (Dead slot)
445                         v.display('EMPTY SLOT')
446                 else:
447                         syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
448                         v.display('UNK ERROR')
449         sleep(1)
450
451
452 def price_check(v, vstatus):
453         if vstatus.cur_selection[1] == '8':
454                 v.display(center('SEE COKE'))
455         else:
456                 # first see if it's a named slot
457                 try:
458                         price, shortname, name = get_snack( vstatus.cur_selection )
459                 except:
460                         price, shortname, name = get_snack( '--' )
461                 dollarprice = "$%.2f" % ( price / 100.0 )
462                 v.display(vstatus.cur_selection+' - %s'%dollarprice)
463
464
465 def handle_getting_pin_key(state, event, params, v, vstatus):
466         #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
467         key = params
468         if len(vstatus.cur_pin) < PIN_LENGTH:
469                 if key == 11:
470                         if vstatus.cur_pin == '':
471                                 vstatus.cur_user = ''
472                                 reset_idler(v, vstatus)
473
474                                 return
475                         vstatus.cur_pin = ''
476                         vstatus.mk.set_message('PIN: ')
477                         return
478                 vstatus.cur_pin += chr(key + ord('0'))
479                 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
480                 if len(vstatus.cur_pin) == PIN_LENGTH:
481                         vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
482                         if vstatus.username:
483                                 v.beep(0, False)
484                                 vstatus.cur_selection = ''
485                                 vstatus.change_state(STATE_GET_SELECTION)
486                                 scroll_options(vstatus.username, vstatus.mk, True)
487                                 return
488                         else:
489                                 v.beep(40, False)
490                                 vstatus.mk.set_messages(
491                                         [(center('BAD PIN'), False, 1.0),
492                                          (center('SORRY'), False, 0.5)])
493                                 vstatus.cur_user = ''
494                                 vstatus.cur_pin = ''
495                         
496                                 reset_idler(v, vstatus, 2)
497
498                                 return
499
500
501 def handle_getting_uid_key(state, event, params, v, vstatus):
502         #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
503         key = params
504
505         # complicated key handling here:
506
507         if len(vstatus.cur_user) == 0 and key == 9:
508                 vstatus.cur_selection = ''
509                 vstatus.time_to_autologout = None
510                 vstatus.mk.set_message('PRICECHECK')
511                 sleep(0.5)
512                 scroll_options('', vstatus.mk)
513                 vstatus.change_state(STATE_GET_SELECTION)
514                 return
515
516         if len(vstatus.cur_user) <8:
517                 if key == 11:
518                         vstatus.cur_user = ''
519
520                         reset_idler(v, vstatus)
521                         return
522                 vstatus.cur_user += chr(key + ord('0'))
523                 #logging.info('dob: '+vstatus.cur_user)
524                 if len(vstatus.cur_user) > 5:
525                         vstatus.mk.set_message('>'+vstatus.cur_user)
526                 else:
527                         vstatus.mk.set_message('UID: '+vstatus.cur_user)
528         
529         if len(vstatus.cur_user) == 5:
530                 uid = int(vstatus.cur_user)
531
532                 if uid == 0:
533                         logging.info('user '+vstatus.cur_user+' has a bad PIN')
534                         pfalken="""
535 CARRIER DETECTED
536
537 CONNECT 128000
538
539 Welcome to Picklevision Sytems, Sunnyvale, CA
540
541 Greetings Professor Falken.
542
543
544
545
546 Shall we play a game?
547
548
549 Please choose from the following menu:
550
551 1. Tic-Tac-Toe
552 2. Chess
553 3. Checkers
554 4. Backgammon
555 5. Poker
556 6. Toxic and Biochemical Warfare
557 7. Global Thermonuclear War
558
559 7 [ENTER]
560
561 Wouldn't you prefer a nice game of chess?
562
563 """.replace('\n','    ')
564                         vstatus.mk.set_messages([(pfalken, False, 10)])
565                         vstatus.cur_user = ''
566                         vstatus.cur_pin = ''
567                         
568                         reset_idler(v, vstatus, 10)
569
570                         return
571
572                 acct_state = get_acct_state(uid)
573                 if acct_state == 'invalid':
574                         logging.info('user '+vstatus.cur_user+' is not in the database')
575                         vstatus.mk.set_messages(
576                                 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
577                         vstatus.cur_user = ''
578                         vstatus.cur_pin = ''
579                         
580                         reset_idler(v, vstatus, 3)
581                         return
582                 elif acct_state == 'locked':
583                         logging.info('user '+vstatus.cur_user+' is locked')
584                         vstatus.mk.set_messages(
585                                 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
586                         vstatus.cur_user = ''
587                         vstatus.cur_pin = ''
588                         
589                         reset_idler(v, vstatus, 3)
590                         return
591                 elif acct_state == 'good':
592                         vstatus.cur_pin = ''
593                         vstatus.mk.set_message('PIN: ')
594                         logging.info('need pin for user %s'%vstatus.cur_user)
595                         vstatus.change_state(STATE_GETTING_PIN)
596                         return
597                 else:
598                         logging.error('user '+vstatus.cur_user+' has an unknown account state'+acct_state)
599                         vstatus.mk.set_messages(
600                                 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
601                         vstatus.cur_user = ''
602                         vstatus.cur_pin = ''
603                         
604                         reset_idler(v, vstatus, 3)
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         
791         if vstatus.username:
792                 v.beep(0, False)
793                 vstatus.cur_selection = ''
794                 vstatus.change_state(STATE_GET_SELECTION)
795                 scroll_options(vstatus.username, vstatus.mk, True)
796                 return
797         else:
798                 v.beep(40, False)
799                 vstatus.mk.set_messages(
800                         [(center('BAD CARD'), False, 1.0),
801                          (center('SORRY'), False, 0.5)])
802                 vstatus.cur_user = ''
803                 vstatus.cur_pin = ''
804         
805                 reset_idler(v, vstatus, 2)
806                 return
807
808 def handle_mifare_add_user_event(state, event, params, v, vstatus):
809         card_id = params
810
811         # Translate card_id into uid.
812         if card_id == None:
813                 return
814
815         try:
816                 if get_uid(card_id) != None:
817                         vstatus.mk.set_messages(
818                                 [(center('ALREADY'), False, 0.5),
819                                  (center('ENROLLED'), False, 0.5)])
820
821                         # scroll_options(vstatus.username, vstatus.mk)
822                         return
823         except ValueError:
824                 pass
825
826         logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
827         set_card_id(vstatus.cur_user, card_id)
828         vstatus.mk.set_messages(
829                 [(center('CARD'), False, 0.5),
830                  (center('ENROLLED'), False, 0.5)])
831
832         # scroll_options(vstatus.username, vstatus.mk)
833
834 def return_to_idle(state,event,params,v,vstatus):
835         reset_idler(v, vstatus)
836
837 def create_state_table(vstatus):
838         vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
839         vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
840         vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
841         vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
842
843         vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
844         vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
845         vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
846         vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
847
848         vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
849         vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
850         vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
851         vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
852
853         vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
854         vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
855         vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
856         vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
857
858         vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
859         vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
860         vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
861         vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
862
863         vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
864         vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
865         vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
866         vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
867
868         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
869         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
870         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
871         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
872         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
873         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
874         vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
875
876 def get_state_table_handler(vstatus, state, event, counter):
877         return vstatus.state_table[(state,event,counter)]
878
879 def time_to_next_update(vstatus):
880         idle_update = vstatus.time_of_next_idlestep - time()
881         if not vstatus.mk.done() and vstatus.mk.next_update is not None:
882                 mk_update = vstatus.mk.next_update - time()
883                 if mk_update < idle_update:
884                         idle_update = mk_update
885         return idle_update
886
887 def run_forever(rfh, wfh, options, cf):
888         v = VendingMachine(rfh, wfh, USE_MIFARE)
889         vstatus = VendState(v)
890         create_state_table(vstatus)
891
892         logging.debug('PING is ' + str(v.ping()))
893
894         if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
895
896         setup_idlers(v)
897         reset_idler(v, vstatus)
898
899         # This main loop was hideous and the work of the devil.
900         # This has now been fixed (mostly) - mtearle
901         #
902         #
903         # notes for later surgery
904         #   (event, counter, ' ')
905         #        V
906         #   d[      ] = (method)
907         #
908         # ( return state - not currently implemented )
909
910         while True:
911                 if USE_DB:
912                         try:
913                                 db.handle_events()
914                         except DispenseDatabaseException, e:
915                                 logging.error('Database error: '+str(e))
916
917                 timeout = time_to_next_update(vstatus)
918                 e = v.next_event(timeout)
919                 (event, params) = e
920
921                 run_handler(event, params, v, vstatus)
922
923 #               logging.debug('Got event: ' + repr(e))
924
925
926 def run_handler(event, params, v, vstatus):
927         handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
928         if handler:
929                 handler(vstatus.state, event, params, v, vstatus)
930
931 def connect_to_vend(options, cf):
932
933         if options.use_lat:
934                 logging.info('Connecting to vending machine using LAT')
935                 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
936                 rfh, wfh = latclient.get_fh()
937         elif options.use_serial:
938                 # Open vending machine via serial.
939                 logging.info('Connecting to vending machine using serial')
940                 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
941                 rfh,wfh = serialclient.get_fh()
942         else:
943                 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
944                 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
945                 import socket
946                 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
947                 sock.connect((options.host, options.port))
948                 rfh = sock.makefile('r')
949                 wfh = sock.makefile('w')
950                 global USE_MIFARE
951                 USE_MIFARE = 0
952                 
953         return rfh, wfh
954
955 def parse_args():
956         from optparse import OptionParser
957
958         op = OptionParser(usage="%prog [OPTION]...")
959         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')
960         op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
961         op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
962         op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
963         op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
964         op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
965         op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
966         op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
967         op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
968         op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
969         op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
970         op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
971         options, args = op.parse_args()
972
973         if len(args) != 0:
974                 op.error('extra command line arguments: ' + ' '.join(args))
975
976         return options
977
978 config_options = {
979         'DBServer': ('Database', 'Server'),
980         'DBName': ('Database', 'Name'),
981         'DBUser': ('VendingMachine', 'DBUser'),
982         'DBPassword': ('VendingMachine', 'DBPassword'),
983         
984         'ServiceName': ('VendingMachine', 'ServiceName'),
985         'ServicePassword': ('VendingMachine', 'Password'),
986         
987         'ServerName': ('DecServer', 'Name'),
988         'ConnectPassword': ('DecServer', 'ConnectPassword'),
989         'PrivPassword': ('DecServer', 'PrivPassword'),
990         }
991
992 class VendConfigFile:
993         def __init__(self, config_file, options):
994                 try:
995                         cp = ConfigParser.ConfigParser()
996                         cp.read(config_file)
997
998                         for option in options:
999                                 section, name = options[option]
1000                                 value = cp.get(section, name)
1001                                 self.__dict__[option] = value
1002                 
1003                 except ConfigParser.Error, e:
1004                         raise SystemExit("Error reading config file "+config_file+": " + str(e))
1005
1006 def create_pid_file(name):
1007         try:
1008                 pid_file = file(name, 'w')
1009                 pid_file.write('%d\n'%os.getpid())
1010                 pid_file.close()
1011         except IOError, e:
1012                 logging.warning('unable to write to pid file '+name+': '+str(e))
1013
1014 def set_stuff_up():
1015         def do_nothing(signum, stack):
1016                 signal.signal(signum, do_nothing)
1017         def stop_server(signum, stack): raise KeyboardInterrupt
1018         signal.signal(signal.SIGHUP, do_nothing)
1019         signal.signal(signal.SIGTERM, stop_server)
1020         signal.signal(signal.SIGINT, stop_server)
1021
1022         options = parse_args()
1023         config_opts = VendConfigFile(options.config_file, config_options)
1024         if options.daemon: become_daemon()
1025         set_up_logging(options)
1026         if options.pid_file != '': create_pid_file(options.pid_file)
1027
1028         return options, config_opts
1029
1030 def clean_up_nicely(options, config_opts):
1031         if options.pid_file != '':
1032                 try:
1033                         os.unlink(options.pid_file)
1034                         logging.debug('Removed pid file '+options.pid_file)
1035                 except OSError: pass  # if we can't delete it, meh
1036
1037 def set_up_logging(options):
1038         logger = logging.getLogger()
1039         
1040         if not options.daemon:
1041                 stderr_logger = logging.StreamHandler(sys.stderr)
1042                 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1043                 logger.addHandler(stderr_logger)
1044         
1045         if options.log_file != '':
1046                 try:
1047                         file_logger = logging.FileHandler(options.log_file)
1048                         file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1049                         logger.addHandler(file_logger)
1050                 except IOError, e:
1051                         logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1052
1053         if options.syslog != None:
1054                 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1055                 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1056                 logger.addHandler(sys_logger)
1057
1058         if options.quiet:
1059                 logger.setLevel(logging.WARNING)
1060         elif options.verbose:
1061                 logger.setLevel(logging.DEBUG)
1062         else:
1063                 logger.setLevel(logging.INFO)
1064
1065 def become_daemon():
1066         dev_null = file('/dev/null')
1067         fd = dev_null.fileno()
1068         os.dup2(fd, 0)
1069         os.dup2(fd, 1)
1070         os.dup2(fd, 2)
1071         try:
1072                 if os.fork() != 0:
1073                         sys.exit(0)
1074                 os.setsid()
1075         except OSError, e:
1076                 raise SystemExit('failed to fork: '+str(e))
1077
1078 def do_vend_server(options, config_opts):
1079         while True:
1080                 try:
1081                         rfh, wfh = connect_to_vend(options, config_opts)
1082                 except (SerialClientException, socket.error), e:
1083                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
1084                         del exc_traceback
1085                         logging.error("Connection error: "+str(exc_type)+" "+str(e))
1086                         logging.info("Trying again in 5 seconds.")
1087                         sleep(5)
1088                         continue
1089                 
1090 #               run_forever(rfh, wfh, options, config_opts)
1091                 
1092                 try:
1093                         run_forever(rfh, wfh, options, config_opts)
1094                 except VendingException as e:
1095                         logging.error("Connection died, trying again...")
1096                         logging.info("Exception: "+e.__str__())
1097                         logging.info("Trying again in 5 seconds.")
1098                         sleep(5)
1099
1100 if __name__ == '__main__':
1101         options, config_opts = set_stuff_up()
1102         while True:
1103                 try:
1104                         logging.warning('Starting Vend Server')
1105                         do_vend_server(options, config_opts)
1106                         logging.error('Vend Server finished unexpectedly, restarting')
1107                 except KeyboardInterrupt:
1108                         logging.info("Killed by signal, cleaning up")
1109                         clean_up_nicely(options, config_opts)
1110                         logging.warning("Vend Server stopped")
1111                         break
1112                 except SystemExit:
1113                         break
1114                 except:
1115                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
1116                         tb = format_tb(exc_traceback, 20)
1117                         del exc_traceback
1118                         
1119                         logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1120                         logging.critical("Message: " + str(exc_value))
1121                         logging.critical("Traceback:")
1122                         for event in tb:
1123                                 for line in event.split('\n'):
1124                                         logging.critical('    '+line)
1125                         logging.critical("This message should be considered a bug in the Vend Server.")
1126                         logging.critical("Please report this to someone who can fix it.")
1127                         sleep(10)
1128                         logging.warning("Trying again anyway (might not help, but hey...)")
1129

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