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