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

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