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

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