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

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