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

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