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

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