VendServer - Log when a vend fails
[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                                 (worked, code, string) = self.v.vend(self.vstatus.cur_selection)
452                                 if worked:
453                                         self.v.display('THANK YOU')
454                                         syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, self.vstatus.cur_selection, self.vstatus.username))
455                                 else:
456                                         print "Vend Failed:", code, string
457                                         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))
458                                         self.v.display('VEND FAIL')
459                         elif (exitcode == 5):   # RV_BALANCE
460                                 self.v.display('NO MONEY?')
461                         elif (exitcode == 4):   # RV_ARGUMENTS (zero give causes arguments)
462                                 self.v.display('EMPTY SLOT')
463                         elif (exitcode == 1):   # RV_BADITEM (Dead slot)
464                                 self.v.display('EMPTY SLOT')
465                         else:
466                                 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))
467                                 self.v.display('UNK ERROR')
468                 sleep(1)
469
470         """
471         Triggered when the user presses a button while entering their pin.
472         """
473         def handle_getting_pin_key(self, event, params):
474                 key = params
475                 if len(self.vstatus.cur_pin) < PIN_LENGTH:
476                         if key == 11:
477                                 if self.vstatus.cur_pin == '':
478                                         self.vstatus.cur_user = ''
479                                         self.dispense.logOut()
480                                         self.reset_idler()
481
482                                         return
483                                 self.vstatus.cur_pin = ''
484                                 self.vstatus.mk.set_message('PIN: ')
485                                 return
486                         self.vstatus.cur_pin += chr(key + ord('0'))
487                         self.vstatus.mk.set_message('PIN: '+'X'*len(self.vstatus.cur_pin))
488                         if len(self.vstatus.cur_pin) == PIN_LENGTH:
489                                 if self.dispense.authUserIdPin(self.vstatus.cur_user, self.vstatus.cur_pin):
490                                         self.vstatus.username = self.dispense.getUsername()
491                                         self.v.beep(0, False)
492                                         self.vstatus.cur_selection = ''
493                                         self.vstatus.change_state(STATE_GET_SELECTION)
494                                         self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
495                                         return
496                                 else:
497                                         self.v.beep(40, False)
498                                         self.vstatus.mk.set_messages(
499                                                 [(self.center('BAD PIN'), False, 1.0),
500                                                  (self.center('SORRY'), False, 0.5)])
501                                         self.vstatus.cur_user = ''
502                                         self.vstatus.cur_pin = ''
503                                 
504                                         self.reset_idler(2)
505
506                                         return
507
508         """
509         Triggered when the user presses a button while entering their user id.
510         """
511         def handle_getting_uid_key(self, event, params):
512                 key = params
513                 # complicated key handling here:
514
515                 if len(self.vstatus.cur_user) == 0 and key == 9:
516                         self.vstatus.cur_selection = ''
517                         self.vstatus.time_to_autologout = None
518                         self.vstatus.mk.set_message('PRICECHECK')
519                         sleep(0.5)
520                         self.scroll_options('', self.vstatus.mk)
521                         self.vstatus.change_state(STATE_GET_SELECTION)
522                         return
523
524                 if len(self.vstatus.cur_user) <8:
525                         if key == 11:
526                                 self.vstatus.cur_user = ''
527                                 self.dispense.logOut()
528
529                                 self.reset_idler()
530                                 return
531                         self.vstatus.cur_user += chr(key + ord('0'))
532                         #logging.info('dob: '+vstatus.cur_user)
533                         if len(self.vstatus.cur_user) > 5:
534                                 self.vstatus.mk.set_message('>'+self.vstatus.cur_user)
535                         else:
536                                 self.vstatus.mk.set_message('UID: '+self.vstatus.cur_user)
537                 
538                 if len(self.vstatus.cur_user) == 5:
539                         uid = int(self.vstatus.cur_user)
540
541                         if uid == 0:
542                                 logging.info('user '+self.vstatus.cur_user+' has a bad PIN')
543                                 pfalken="""
544         CARRIER DETECTED
545
546         CONNECT 128000
547
548         Welcome to Picklevision Sytems, Sunnyvale, CA
549
550         Greetings Professor Falken.
551
552
553
554
555         Shall we play a game?
556
557
558         Please choose from the following menu:
559
560         1. Tic-Tac-Toe
561         2. Chess
562         3. Checkers
563         4. Backgammon
564         5. Poker
565         6. Toxic and Biochemical Warfare
566         7. Global Thermonuclear War
567
568         7 [ENTER]
569
570         Wouldn't you prefer a nice game of chess?
571
572         """.replace('\n','    ')
573                                 self.vstatus.mk.set_messages([(pfalken, False, 10)])
574                                 self.vstatus.cur_user = ''
575                                 self.vstatus.cur_pin = ''
576                                 
577                                 self.reset_idler(10)
578
579                                 return
580
581                         # TODO Fix this up do we can check before logging in
582                         """
583                         if self.dispense.isDisabled():
584                                 logging.info('user '+self.vstatus.cur_user+' is disabled')
585                                 self.vstatus.mk.set_messages(
586                                         [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
587                                 self.vstatus.cur_user = ''
588                                 self.vstatus.cur_pin = ''
589                                 
590                                 self.reset_idler(3)
591                                 return
592                         """
593
594                         self.vstatus.cur_pin = ''
595                         self.vstatus.mk.set_message('PIN: ')
596                         logging.info('need pin for user %s'%self.vstatus.cur_user)
597                         self.vstatus.change_state(STATE_GETTING_PIN)
598                         return
599
600         """
601         Triggered when a key is pressed and the machine is idling.
602         """
603         def handle_idle_key(self, event, params):
604                 key = params
605                 if key == 11:
606                         self.vstatus.cur_user = ''
607                         self.dispense.logOut()
608                         self.reset_idler()
609                         return
610                 
611                 self.vstatus.change_state(STATE_GETTING_UID)
612                 self.run_handler(event, params)
613
614         """
615         What to do when there is nothing to do.
616         """
617         def handle_idle_tick(self, event, params):
618                 ### State idling
619                 if self.vstatus.mk.done():
620                         self.idle_step()
621
622                 if self.vstatus.time_of_next_idler and time() > self.vstatus.time_of_next_idler:
623                         self.vstatus.time_of_next_idler = time() + 30
624                         self.choose_idler()
625                 
626                 ###
627
628                 self.vstatus.mk.update_display()
629
630                 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK)
631                 self.run_handler(event, params)
632                 sleep(0.05)
633
634         """
635         Manages the beeps for the grandfather clock
636         """
637         def beep_on(self, when, before=0):
638                 start = int(when - before)
639                 end = int(when)
640                 now = int(time())
641
642                 if now >= start and now <= end:
643                         return 1
644                 return 0
645
646         def handle_idle_grandfather_tick(self, event, params):
647                 ### check for interesting times
648                 now = localtime()
649
650                 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
651                 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
652                 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
653                 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
654
655                 hourfromnow = localtime(time() + 3600)
656                 
657                 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
658                 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
659                         0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
660
661                 ## check for X seconds to the hour
662                 ## if case, update counter to 2
663                 if self.beep_on(onthehour,15) \
664                         or self.beep_on(halfhour,0) \
665                         or self.beep_on(quarterhour,0) \
666                         or self.beep_on(threequarterhour,0) \
667                         or self.beep_on(fivetothehour,0):
668                         self.vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
669                         self.run_handler(event, params)
670                 else:
671                         self.vstatus.change_state(STATE_IDLE)
672
673         def handle_grandfather_tick(self, event, params):
674                 go_idle = 1
675
676                 msg = []
677                 ### we live in interesting times
678                 now = localtime()
679
680                 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
681                 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
682                 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
683                 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
684
685                 hourfromnow = localtime(time() + 3600)
686                 
687         #       onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
688                 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
689                         0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
690
691
692                 #print "when it fashionable to wear a onion on your hip"
693
694                 if self.beep_on(onthehour,15):
695                         go_idle = 0
696                         next_hour=((hourfromnow[3] + 11) % 12) + 1
697                         if onthehour - time() < next_hour and onthehour - time() > 0:
698                                 self.v.beep(0, False)
699
700                                 t = int(time())
701                                 if (t % 2) == 0:
702                                         msg.append(("DING!", False, None))
703                                 else:
704                                         msg.append(("     DING!", False, None))
705                         elif int(onthehour - time()) == 0:
706                                 self.v.beep(255, False)
707                                 msg.append(("   BONG!", False, None))
708                                 msg.append(("     IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
709                 elif self.beep_on(halfhour,0):
710                         go_idle = 0
711                         self.v.beep(0, False)
712                         msg.append((" HALFHOUR ", False, 50))
713                 elif self.beep_on(quarterhour,0):
714                         go_idle = 0
715                         self.v.beep(0, False)
716                         msg.append((" QTR HOUR ", False, 50))
717                 elif self.beep_on(threequarterhour,0):
718                         go_idle = 0
719                         self.v.beep(0, False)
720                         msg.append((" 3 QTR HR ", False, 50))
721                 elif self.beep_on(fivetothehour,0):
722                         go_idle = 0
723                         self.v.beep(0, False)
724                         msg.append(("Quick run to your lectures!  Hurry! Hurry!", False, TEXT_SPEED*4))
725                 else:
726                         go_idle = 1
727                 
728                 ## check for X seconds to the hour
729
730                 if len(msg):
731                         self.vstatus.mk.set_messages(msg)
732                         sleep(1)
733
734                 self.vstatus.mk.update_display()
735                 ## if no longer case, return to idle
736
737                 ## change idler to be clock
738                 if go_idle and self.vstatus.mk.done():
739                         self.vstatus.change_state(STATE_IDLE,1)
740
741         """
742         What to do when the door is open.
743         """
744         def handle_door_idle(self, event, params):
745                 def twiddle(clock,v,wise = 2):
746                         if (clock % 4 == 0):
747                                 v.display("-FEED  ME-")
748                         elif (clock % 4 == 1+wise):
749                                 v.display("\\FEED  ME/")
750                         elif (clock % 4 == 2):
751                                 v.display("-FEED  ME-")
752                         elif (clock % 4 == 3-wise):
753                                 v.display("/FEED  ME\\")
754
755                 # don't care right now.
756                 now = int(time())
757
758                 if ((now % 60 % 2) == 0):
759                         twiddle(now, self.v)
760                 else:
761                         twiddle(now, self.v, wise=0)
762
763         """
764         What to do when the door is opened or closed.
765         """
766         def handle_door_event(self, event, params):
767                 if params == 0:  #door open
768                         self.vstatus.change_state(STATE_DOOR_OPENING)
769                         logging.warning("Entering open door mode")
770                         self.v.display("-FEED  ME-")
771                         #door_open_mode(v);
772                         self.dispense.logOut()
773                         self.vstatus.cur_user = ''
774                         self.vstatus.cur_pin = ''
775                 elif params == 1:  #door closed
776                         self.vstatus.change_state(STATE_DOOR_CLOSING)
777                         self.reset_idler(3)
778
779                         logging.warning('Leaving open door mode')
780                         self.v.display("-YUM YUM!-")
781
782         """
783         Triggered when a user swipes their caed, and the machine is logged out.
784         """
785         def handle_mifare_event(self, event, params):
786                 card_id = params
787                 # Translate card_id into uid.
788                 if card_id == None or card_id == self._last_card_id:
789                         return
790
791                 self._last_card_id = card_id
792                 
793                 if not self.dispense.authMifareCard(card_id):
794                         self.v.beep(40, False)
795                         self.vstatus.mk.set_messages(
796                                 [(self.center('BAD CARD'), False, 1.0),
797                                  (self.center('SORRY'), False, 0.5)])
798                         self.vstatus.cur_user = ''
799                         self.vstatus.cur_pin = ''
800                         self._last_card_id = -1
801                 
802                         self.reset_idler(2)
803                         return
804                 elif self.dispense.isDisabled():
805                         logging.info('Mapped card id to uid %s'%self.dispense.getUsername())
806                         self.v.beep(40, False)
807                         self.vstatus.mk.set_messages(
808                                 [(self.center('ACCT DISABLED'), False, 1.0),
809                                  (self.center('SORRY'), False, 0.5)])
810                         self.dispense.logOut()
811                         self.reset_idler(2)
812                         return
813                 else:
814                         logging.info('Mapped card id to uid %s'%self.dispense.getUsername())
815                         self.vstatus.cur_user = '----'
816                         self.vstatus.username = self.dispense.getUsername()
817                         self.vstatus.cur_selection = ''
818                         self.vstatus.change_state(STATE_GET_SELECTION)
819                         self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
820                         return
821
822         """
823         Triggered when a user swipes their card and the machine is logged in.
824         """
825         def handle_mifare_add_user_event(self, event, params):
826                 card_id = params
827
828                 # Translate card_id into uid.
829                 if card_id == None or card_id == self._last_card_id:
830                         return
831
832                 self._last_card_id = card_id
833
834                 if not self.dispense.addCard(card_id):
835                         self.vstatus.mk.set_messages(
836                                 [(self.center('ALREADY'), False, 0.5),
837                                  (self.center('ENROLLED'), False, 0.5)])
838                 else:
839                         self.vstatus.mk.set_messages(
840                                 [(self.center('CARD'), False, 0.5),
841                                  (self.center('ENROLLED'), False, 0.5)])
842
843         def return_to_idle(self, event, params):
844                 self.reset_idler()
845
846         """
847         Maps what to do when the state changes.
848         """
849         def create_state_table(self):
850                 self.vstatus.state_table[(STATE_IDLE,TICK,1)] = self.handle_idle_tick
851                 self.vstatus.state_table[(STATE_IDLE,KEY,1)] = self.handle_idle_key
852                 self.vstatus.state_table[(STATE_IDLE,DOOR,1)] = self.handle_door_event
853                 self.vstatus.state_table[(STATE_IDLE,MIFARE,1)] = self.handle_mifare_event
854
855                 self.vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = self.handle_door_idle
856                 self.vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = self.handle_door_event
857                 self.vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = self.do_nothing
858                 self.vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = self.do_nothing
859
860                 self.vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = self.return_to_idle
861                 self.vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = self.handle_door_event
862                 self.vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = self.do_nothing
863                 self.vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = self.do_nothing
864
865                 self.vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = self.handle_getting_uid_idle
866                 self.vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = self.handle_door_event
867                 self.vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = self.handle_getting_uid_key
868                 self.vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = self.handle_mifare_event
869
870                 self.vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = self.handle_getting_pin_idle
871                 self.vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = self.handle_door_event
872                 self.vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = self.handle_getting_pin_key
873                 self.vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = self.handle_mifare_event
874
875                 self.vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = self.handle_get_selection_idle
876                 self.vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = self.handle_door_event
877                 self.vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = self.handle_get_selection_key
878                 self.vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = self.handle_mifare_add_user_event
879
880                 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = self.handle_idle_grandfather_tick
881                 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = self.handle_grandfather_tick
882                 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = self.handle_door_event
883                 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = self.handle_door_event
884                 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = self.do_nothing
885                 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = self.do_nothing
886                 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = self.handle_mifare_event
887
888         """
889         Get what to do on a state change.
890         """
891         def get_state_table_handler(self, state, event, counter):
892                 return self.vstatus.state_table[(state,event,counter)]
893
894         def time_to_next_update(self):
895                 idle_update = self.vstatus.time_of_next_idlestep - time()
896                 if not self.vstatus.mk.done() and self.vstatus.mk.next_update is not None:
897                         mk_update = self.vstatus.mk.next_update - time()
898                         if mk_update < idle_update:
899                                 idle_update = mk_update
900                 return idle_update
901
902         def run_forever(self, rfh, wfh, options, cf):
903                 self.v = VendingMachine(rfh, wfh, USE_MIFARE)
904                 self.dispense = Dispense()
905                 self.vstatus = VendState(self.v)
906                 self.create_state_table()
907
908                 logging.debug('PING is ' + str(self.v.ping()))
909
910                 self.setup_idlers()
911                 self.reset_idler()
912
913                 while True:
914                         timeout = self.time_to_next_update()
915                         (event, params) = self.v.next_event(timeout)
916                         self.run_handler(event, params)
917
918         def run_handler(self, event, params):
919                 handler = self.get_state_table_handler(self.vstatus.state,event,self.vstatus.counter)
920                 if handler:
921                         handler(event, params)
922
923 """
924 Connect to the machine.
925 """
926 def connect_to_vend(options, cf):
927
928         if options.use_lat:
929                 logging.info('Connecting to vending machine using LAT')
930                 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
931                 rfh, wfh = latclient.get_fh()
932         elif options.use_serial:
933                 # Open vending machine via serial.
934                 logging.info('Connecting to vending machine using serial')
935                 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
936                 rfh,wfh = serialclient.get_fh()
937         else:
938                 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
939                 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
940                 import socket
941                 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
942                 sock.connect((options.host, options.port))
943                 rfh = sock.makefile('r')
944                 wfh = sock.makefile('w')
945                 global USE_MIFARE
946                 USE_MIFARE = 0
947                 
948         return rfh, wfh
949
950 """
951 Parse arguments from the command line
952 """
953 def parse_args():
954         from optparse import OptionParser
955
956         op = OptionParser(usage="%prog [OPTION]...")
957         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')
958         op.add_option('--serial', action='store_true', default=False, dest='use_serial', help='use the serial port')
959         op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
960         op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
961         op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
962         op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
963         op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
964         op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
965         op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
966         op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
967         op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
968         op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
969         options, args = op.parse_args()
970
971         if len(args) != 0:
972                 op.error('extra command line arguments: ' + ' '.join(args))
973
974         return options
975
976 def create_pid_file(name):
977         try:
978                 pid_file = file(name, 'w')
979                 pid_file.write('%d\n'%os.getpid())
980                 pid_file.close()
981         except IOError, e:
982                 logging.warning('unable to write to pid file '+name+': '+str(e))
983
984 def set_stuff_up():
985         def do_nothing(signum, stack):
986                 signal.signal(signum, do_nothing)
987         def stop_server(signum, stack): raise KeyboardInterrupt
988         signal.signal(signal.SIGHUP, do_nothing)
989         signal.signal(signal.SIGTERM, stop_server)
990         signal.signal(signal.SIGINT, stop_server)
991
992         options = parse_args()
993         config_opts = VendConfigFile(options.config_file, config_options)
994         if options.daemon: become_daemon()
995         set_up_logging(options)
996         if options.pid_file != '': create_pid_file(options.pid_file)
997
998         return options, config_opts
999
1000 def clean_up_nicely(options, config_opts):
1001         if options.pid_file != '':
1002                 try:
1003                         os.unlink(options.pid_file)
1004                         logging.debug('Removed pid file '+options.pid_file)
1005                 except OSError: pass  # if we can't delete it, meh
1006
1007 def set_up_logging(options):
1008         logger = logging.getLogger()
1009         
1010         if not options.daemon:
1011                 stderr_logger = logging.StreamHandler(sys.stderr)
1012                 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1013                 logger.addHandler(stderr_logger)
1014         
1015         if options.log_file != '':
1016                 try:
1017                         file_logger = logging.FileHandler(options.log_file)
1018                         file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1019                         logger.addHandler(file_logger)
1020                 except IOError, e:
1021                         logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1022
1023         if options.syslog != None:
1024                 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1025                 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1026                 logger.addHandler(sys_logger)
1027
1028         if options.quiet:
1029                 logger.setLevel(logging.WARNING)
1030         elif options.verbose:
1031                 logger.setLevel(logging.DEBUG)
1032         else:
1033                 logger.setLevel(logging.INFO)
1034
1035 def become_daemon():
1036         dev_null = file('/dev/null')
1037         fd = dev_null.fileno()
1038         os.dup2(fd, 0)
1039         os.dup2(fd, 1)
1040         os.dup2(fd, 2)
1041         try:
1042                 if os.fork() != 0:
1043                         sys.exit(0)
1044                 os.setsid()
1045         except OSError, e:
1046                 raise SystemExit('failed to fork: '+str(e))
1047
1048 def do_vend_server(options, config_opts):
1049         while True:
1050                 try:
1051                         rfh, wfh = connect_to_vend(options, config_opts)
1052                 except (SerialClientException, socket.error), e:
1053                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
1054                         del exc_traceback
1055                         logging.error("Connection error: "+str(exc_type)+" "+str(e))
1056                         logging.info("Trying again in 5 seconds.")
1057                         sleep(5)
1058                         continue
1059                 
1060 #               run_forever(rfh, wfh, options, config_opts)
1061                 
1062                 try:
1063                         vserver = VendServer()
1064                         vserver.run_forever(rfh, wfh, options, config_opts)
1065                 except VendingException:
1066                         logging.error("Connection died, trying again...")
1067                         logging.info("Trying again in 5 seconds.")
1068                         sleep(5)
1069
1070
1071 def main(argv=None):
1072         options, config_opts = set_stuff_up()
1073         while True:
1074                 try:
1075                         logging.warning('Starting Vend Server')
1076                         do_vend_server(options, config_opts)
1077                         logging.error('Vend Server finished unexpectedly, restarting')
1078                 except KeyboardInterrupt:
1079                         logging.info("Killed by signal, cleaning up")
1080                         clean_up_nicely(options, config_opts)
1081                         logging.warning("Vend Server stopped")
1082                         break
1083                 except SystemExit:
1084                         break
1085                 except:
1086                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
1087                         tb = format_tb(exc_traceback, 20)
1088                         del exc_traceback
1089                         
1090                         logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1091                         logging.critical("Message: " + str(exc_value))
1092                         logging.critical("Traceback:")
1093                         for event in tb:
1094                                 for line in event.split('\n'):
1095                                         logging.critical('    '+line)
1096                         logging.critical("This message should be considered a bug in the Vend Server.")
1097                         logging.critical("Please report this to someone who can fix it.")
1098                         sleep(10)
1099                         logging.warning("Trying again anyway (might not help, but hey...)")
1100
1101 if __name__ == '__main__':
1102         sys.exit(main())

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