Removed large chunks of unused code
[uccvend-vendserver.git] / VendServer / VendServer.py
1 #!/usr/bin/python
2 # vim:ts=4
3
4 USE_DB = 0
5 USE_MIFARE = 1
6
7 import ConfigParser
8 import sys, os, string, re, pwd, signal, math, syslog
9 import logging, logging.handlers
10 from traceback import format_tb
11 if USE_DB: import pg
12 from time import time, sleep, mktime, localtime
13 from subprocess import Popen, PIPE
14 from LATClient import LATClient, LATClientException
15 from SerialClient import SerialClient, SerialClientException
16 from VendingMachine import VendingMachine, VendingException
17 from MessageKeeper import MessageKeeper
18 from HorizScroll import HorizScroll
19 from random import random, seed
20 from Idler import GreetingIdler,TrainIdler,GrayIdler,StringIdler,ClockIdler,FortuneIdler,FileIdler,PipeIdler
21 from SnackConfig import get_snack#, get_snacks
22 import socket
23 from posix import geteuid
24 from LDAPConnector import get_uid,get_uname, set_card_id
25 from OpenDispense import OpenDispense as Dispense
26
27 CREDITS="""
28 This vending machine software brought to you by:
29 Bernard Blackham
30 Mark Tearle
31 Nick Bannon
32 Cameron Patrick
33 and a collective of hungry alpacas.
34
35 The MIFARE card reader bought to you by:
36 David Adam
37
38 Bug Hunting and hardware maintenance by:
39 Mitchell Pomery
40
41 For a good time call +61 8 6488 3901
42
43
44
45 """
46
47 PIN_LENGTH = 4
48
49 DOOR = 1
50 SWITCH = 2
51 KEY = 3
52 TICK = 4
53 MIFARE = 5
54
55
56 (
57 STATE_IDLE,
58 STATE_DOOR_OPENING,
59 STATE_DOOR_CLOSING,
60 STATE_GETTING_UID,
61 STATE_GETTING_PIN,
62 STATE_GET_SELECTION,
63 STATE_GRANDFATHER_CLOCK,
64 ) = range(1,8)
65
66 TEXT_SPEED = 0.8
67 IDLE_SPEED = 0.05
68
69
70 config_options = {
71         'DBServer': ('Database', 'Server'),
72         'DBName': ('Database', 'Name'),
73         'DBUser': ('VendingMachine', 'DBUser'),
74         'DBPassword': ('VendingMachine', 'DBPassword'),
75         
76         'ServiceName': ('VendingMachine', 'ServiceName'),
77         'ServicePassword': ('VendingMachine', 'Password'),
78         
79         'ServerName': ('DecServer', 'Name'),
80         'ConnectPassword': ('DecServer', 'ConnectPassword'),
81         'PrivPassword': ('DecServer', 'PrivPassword'),
82         }
83
84 class VendConfigFile:
85         def __init__(self, config_file, options):
86                 try:
87                         cp = ConfigParser.ConfigParser()
88                         cp.read(config_file)
89
90                         for option in options:
91                                 section, name = options[option]
92                                 value = cp.get(section, name)
93                                 self.__dict__[option] = value
94                 
95                 except ConfigParser.Error, e:
96                         raise SystemExit("Error reading config file "+config_file+": " + str(e))
97
98 class DispenseDatabaseException(Exception): pass
99
100 class DispenseDatabase:
101         def __init__(self, vending_machine, host, name, user, password):
102                 self.vending_machine = vending_machine
103                 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
104                 self.db.query('LISTEN vend_requests')
105
106         def process_requests(self):
107                 logging.debug('database processing')
108                 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
109                 try:
110                         outstanding = self.db.query(query).getresult()
111                 except (pg.error,), db_err:
112                         raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
113                 for (id, slot) in outstanding:
114                         (worked, code, string) = self.vending_machine.vend(slot)
115                         logging.debug (str((worked, code, string)))
116                         if worked:
117                                 query = 'SELECT vend_success(%s)'%id
118                                 self.db.query(query).getresult()
119                         else:
120                                 query = 'SELECT vend_failed(%s)'%id
121                                 self.db.query(query).getresult()
122
123         def handle_events(self):
124                 notifier = self.db.getnotify()
125                 while notifier is not None:
126                         self.process_requests()
127                         notify = self.db.getnotify()
128 """
129 This class manages the current state of the vending machine.
130 """
131 class VendState:
132         def __init__(self,v):
133                 self.state_table = {}
134                 self.state = STATE_IDLE
135                 self.counter = 0
136
137                 self.mk = MessageKeeper(v)
138                 self.cur_user = ''
139                 self.cur_pin = ''
140                 self.username = ''
141                 self.cur_selection = ''
142                 self.time_to_autologout = None
143
144                 self.last_timeout_refresh = None
145
146         def change_state(self,newstate,newcounter=None):
147                 if self.state != newstate:
148                         self.state = newstate
149
150                 if newcounter is not None and self.counter != newcounter:
151                         self.counter = newcounter
152
153 class VendServer():
154
155         v = None
156         vstatus = None
157         state = None
158         event = None
159         idlers = []
160         idler = None
161
162         _pin_uid = 0
163         _pin_uname = 'root'
164         _pin_pin = '----'
165         _last_card_id = -1
166
167         dispense = None
168
169         """
170         Show information to the user as to what can be dispensed.
171         """
172         def scroll_options(self, username, mk, welcome = False):
173                 # If the user has just logged in, show them their balance
174                 if welcome:
175                         balance = self.dispense.getBalance()
176                         
177                         msg = [(self.center('WELCOME'), False, TEXT_SPEED),
178                                    (self.center(self.dispense.getUsername()), False, TEXT_SPEED),
179                                    (self.center(balance), False, TEXT_SPEED),]
180                 else:
181                         msg = []
182                 choices = ' '*10+'CHOICES: '
183
184                 # Show what is in the coke machine
185                 # Need to update this so it uses the abstracted system
186                 cokes = []
187                 for i in ['08', '18', '28', '38', '48', '58', '68']:
188                         cokes.append((i, self.dispense.getItemInfo(i)))
189
190                 for c in cokes:
191                         if c[1][0] == 'dead':
192                                 continue
193                         choices += '%s-(%sc)-%s8 '%(c[1][0], c[1][1], c[0])
194
195                 # Show the final few options
196                 choices += '55-DOOR '
197                 choices += 'OR ANOTHER SNACK. '
198                 choices += '99 TO READ AGAIN. '
199                 choices += 'CHOICE?   '
200                 msg.append((choices, False, None))
201                 # Send it to the display
202                 mk.set_messages(msg)
203
204         """
205         In here just for fun.
206         """
207         def cookie(self):
208                 seed(time())
209                 messages = ['  WASSUP! ', 'PINK FISH ', ' SECRETS ', '  ESKIMO  ', ' FORTUNES ', 'MORE MONEY']
210                 choice = int(random()*len(messages))
211                 msg = messages[choice]
212                 left = range(len(msg))
213                 for i in range(len(msg)):
214                         if msg[i] == ' ': left.remove(i)
215                 reveal = 1
216                 while left:
217                         s = ''
218                         for i in range(0, len(msg)):
219                                 if i in left:
220                                         if reveal == 0:
221                                                 left.remove(i)
222                                                 s += msg[i]
223                                         else:
224                                                 s += chr(int(random()*26)+ord('A'))
225                                         reveal += 1
226                                         reveal %= 17
227                                 else:
228                                         s += msg[i]
229                         self.v.display(s)
230
231         """
232         Format text so it will appear centered on the screen.
233         """
234         def center(self, str):
235                 LEN = 10
236                 return ' '*((LEN-len(str))/2)+str
237
238         """
239         Configure the things that will appear on screen whil the machine is idling.
240         """
241         def setup_idlers(self):
242                 self.idlers = [
243                         #
244                         GrayIdler(self.v),
245                         GrayIdler(self.v,one="*",zero="-"),
246                         GrayIdler(self.v,one="/",zero="\\"),
247                         GrayIdler(self.v,one="X",zero="O"),
248                         GrayIdler(self.v,one="*",zero="-",reorder=1),
249                         GrayIdler(self.v,one="/",zero="\\",reorder=1),
250                         GrayIdler(self.v,one="X",zero="O",reorder=1),
251                         #
252                         ClockIdler(self.v),
253                         ClockIdler(self.v),
254                         ClockIdler(self.v),
255                         #
256                         StringIdler(self.v), # Hello Cruel World
257                         StringIdler(self.v, text="Kill 'em all", repeat=False),
258                         StringIdler(self.v, text=CREDITS),
259                         StringIdler(self.v, text=str(math.pi) + "            "),
260                         StringIdler(self.v, text=str(math.e) + "            "),
261                         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),
262                         # "Hello World" in brainfuck
263                         StringIdler(self.v, text=">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-] <.>+++++++++++[<++++++++>-]<-.--------.+++.------.--------.[-]>++++++++[<++++>- ]<+.[-]++++++++++."),
264                         #
265                         TrainIdler(self.v),
266                         #
267                         FileIdler(self.v, '/usr/share/common-licenses/GPL-2',affinity=2),
268                         #
269                         PipeIdler(self.v, "/usr/bin/getent", "passwd"),
270                         FortuneIdler(self.v,affinity=20),
271                         ]
272                 disabled = [
273                         ]
274
275         """
276         Go back to the default idler.
277         """
278         def reset_idler(self, t = None):
279                 self.idler = GreetingIdler(self.v, t)
280                 self.vstatus.time_of_next_idlestep = time()+self.idler.next()
281                 self.vstatus.time_of_next_idler = None
282                 self.vstatus.time_to_autologout = None
283                 self.vstatus.change_state(STATE_IDLE, 1)
284
285         """
286         Change to a random idler.
287         """
288         def choose_idler(self):
289
290                 # Implementation of the King Of the Hill algorithm from;
291                 # http://eli.thegreenplace.net/2010/01/22/weighted-random-generation-in-python/
292
293                 #def weighted_choice_king(weights):
294                 #       total = 0
295                 #       winner = 0
296                 #       for i, w in enumerate(weights):
297                 #               total += w
298                 #               if random.random() * total < w:
299                 #                       winner = i
300                 #       return winner
301                 #       
302
303                 total = 0
304                 winner = None
305                 
306                 for choice in self.idlers:
307                         weight = choice.affinity()
308                         total += weight
309                         if random() * total < weight:
310                                 winner = choice
311
312                 self.idler = winner
313
314                 if self.idler:
315                         self.idler.reset()
316         """
317         Run every step while the machine is idling.
318         """
319         def idle_step(self):
320                 if self.idler.finished():
321                         self.choose_idler()
322                         self.vstatus.time_of_next_idler = time() + 30
323                 nextidle = self.idler.next()
324                 if nextidle is None:
325                         nextidle = IDLE_SPEED
326                 self.vstatus.time_of_next_idlestep = time()+nextidle
327
328         """
329         These next two events trigger no response in the code.
330         """
331         def handle_tick_event(self, event, params):
332                 # don't care right now.
333                 pass
334
335         def handle_switch_event(self, event, params):
336                 # don't care right now.
337                 pass
338
339         """
340         Don't do anything for this event.
341         """
342         def do_nothing(self, event, params):
343                 print "doing nothing (s,e,p)", state, " ", event, " ", params
344                 pass
345
346         """
347         These next few entrie tell us to do nothing while we are idling
348         """
349         def handle_getting_uid_idle(self, event, params):
350                 # don't care right now.
351                 pass
352
353         def handle_getting_pin_idle(self, event, params):
354                 # don't care right now.
355                 pass
356
357         """
358         While logged in and waiting for user input, slowly get closer to logging out.
359         """
360         def handle_get_selection_idle(self, event, params):
361                 # don't care right now.
362                 ###
363                 ### State logging out ..
364                 if self.vstatus.time_to_autologout != None:
365                         time_left = self.vstatus.time_to_autologout - time()
366                         if time_left < 6 and (self.vstatus.last_timeout_refresh is None or self.vstatus.last_timeout_refresh > time_left):
367                                 self.vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
368                                 self.vstatus.last_timeout_refresh = int(time_left)
369                                 self.vstatus.cur_selection = ''
370
371                 if self.vstatus.time_to_autologout != None and self.vstatus.time_to_autologout - time() <= 0:
372                         self.vstatus.time_to_autologout = None
373                         self.vstatus.cur_user = ''
374                         self.vstatus.cur_pin = ''
375                         self.vstatus.cur_selection = ''
376                         self._last_card_id = -1
377                         self.reset_idler()
378
379                 ### State fully logged out ... reset variables
380                 if self.vstatus.time_to_autologout and not self.vstatus.mk.done(): 
381                         self.vstatus.time_to_autologout = None
382                 if self.vstatus.cur_user == '' and self.vstatus.time_to_autologout: 
383                         self.vstatus.time_to_autologout = None
384                 
385                 ### State logged in
386                 if len(self.vstatus.cur_pin) == PIN_LENGTH and self.vstatus.mk.done() and self.vstatus.time_to_autologout == None:
387                         # start autologout
388                         self.vstatus.time_to_autologout = time() + 15
389                         self.vstatus.last_timeout_refresh = None
390
391                 ## FIXME - this may need to be elsewhere.....
392                 # need to check
393                 self.vstatus.mk.update_display()
394
395         """
396         Triggered on user input while logged in.
397         """
398         def handle_get_selection_key(self, event, params):
399                 key = params
400                 if len(self.vstatus.cur_selection) == 0:
401                         if key == 11:
402                                 self.vstatus.cur_pin = ''
403                                 self.vstatus.cur_user = ''
404                                 self.vstatus.cur_selection = ''
405                                 _last_card_id = -1
406                                 self.vstatus.mk.set_messages([(self.center('BYE!'), False, 1.5)])
407                                 self.reset_idler(2)
408                                 return
409                         self.vstatus.cur_selection += chr(key + ord('0'))
410                         self.vstatus.mk.set_message('SELECT: '+self.vstatus.cur_selection)
411                         self.vstatus.time_to_autologout = None
412                 elif len(self.vstatus.cur_selection) == 1:
413                         if key == 11:
414                                 self.vstatus.cur_selection = ''
415                                 self.vstatus.time_to_autologout = None
416                                 self.scroll_options(self.vstatus.username, self.vstatus.mk)
417                                 return
418                         else:
419                                 self.vstatus.cur_selection += chr(key + ord('0'))
420                                 if self.vstatus.cur_user:
421                                         self.make_selection()
422                                         self.vstatus.cur_selection = ''
423                                         self.vstatus.time_to_autologout = time() + 8
424                                         self.vstatus.last_timeout_refresh = None
425                                 else:
426                                         # Price check mode.
427                                         self.dispense.getItemInfo(self.vstatus.cur_selection)
428                                         self.vstatus.cur_selection = ''
429                                         self.vstatus.time_to_autologout = None
430                                         self.vstatus.last_timeout_refresh = None
431
432         """
433         Triggered when the user has entered the id of something they would like to purchase.
434         """
435         def make_selection(self):
436                 # should use sudo here
437                 if self.vstatus.cur_selection == '55':
438                         self.vstatus.mk.set_message('OPENSESAME')
439                         logging.info('dispensing a door for %s'%self.vstatus.username)
440                         if geteuid() == 0:
441                                 ret = os.system('dispense -u "%s" door'%self.vstatus.username)
442                         else:
443                                 ret = os.system('dispense door')
444                         if ret == 0:
445                                 logging.info('door opened')
446                                 self.vstatus.mk.set_message(self.center('DOOR OPEN'))
447                         else:
448                                 logging.warning('user %s tried to dispense a bad door'%self.vstatus.username)
449                                 self.vstatus.mk.set_message(self.center('BAD DOOR'))
450                         sleep(1)
451                 elif self.vstatus.cur_selection == '81':
452                         self.cookie()
453                 elif self.vstatus.cur_selection == '99':
454                         self.scroll_options(self.vstatus.username, self.vstatus.mk)
455                         self.vstatus.cur_selection = ''
456                         return
457                 elif self.vstatus.cur_selection[1] == '8':
458                         self.v.display('GOT DRINK?')
459                         if ((os.system('dispense -u "%s" coke:%s'%(self.vstatus.username, self.vstatus.cur_selection[0])) >> 8) != 0):
460                                 self.v.display('SEEMS NOT')
461                         else:
462                                 self.v.display('GOT DRINK!')
463                 else:
464                         # first see if it's a named slot
465                         try:
466                                 price, shortname, name = get_snack( self.vstatus.cur_selection )
467                         except:
468                                 price, shortname, name = get_snack( '--' )
469                         dollarprice = "$%.2f" % ( price / 100.0 )
470                         self.v.display(self.vstatus.cur_selection+' - %s'%dollarprice)
471                         exitcode = os.system('dispense -u "%s" snack:%s'%(self.vstatus.username, self.vstatus.cur_selection)) >> 8
472                         if (exitcode == 0):
473                                 # magic dispense syslog service
474                                 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, self.vstatus.cur_selection, self.vstatus.username))
475                                 (worked, code, string) = self.v.vend(self.vstatus.cur_selection)
476                                 if worked:
477                                         self.v.display('THANK YOU')
478                                 else:
479                                         print "Vend Failed:", code, string
480                                         self.v.display('VEND FAIL')
481                         elif (exitcode == 5):   # RV_BALANCE
482                                 self.v.display('NO MONEY?')
483                         elif (exitcode == 4):   # RV_ARGUMENTS (zero give causes arguments)
484                                 self.v.display('EMPTY SLOT')
485                         elif (exitcode == 1):   # RV_BADITEM (Dead slot)
486                                 self.v.display('EMPTY SLOT')
487                         else:
488                                 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))
489                                 self.v.display('UNK ERROR')
490                 sleep(1)
491
492         """
493         Triggered when the user presses a button while entering their pin.
494         """
495         def handle_getting_pin_key(self, event, params):
496                 key = params
497                 if len(self.vstatus.cur_pin) < PIN_LENGTH:
498                         if key == 11:
499                                 if self.vstatus.cur_pin == '':
500                                         self.vstatus.cur_user = ''
501                                         slef.reset_idler()
502
503                                         return
504                                 self.vstatus.cur_pin = ''
505                                 self.vstatus.mk.set_message('PIN: ')
506                                 return
507                         self.vstatus.cur_pin += chr(key + ord('0'))
508                         self.vstatus.mk.set_message('PIN: '+'X'*len(self.vstatus.cur_pin))
509                         if len(self.vstatus.cur_pin) == PIN_LENGTH:
510                                 self.dispense.authUserIdPin(self.vstatus.cur_user, self.vstatus.cur_pin)
511                                 if self.dispense.getUsername():
512                                         self.v.beep(0, False)
513                                         self.vstatus.cur_selection = ''
514                                         self.vstatus.change_state(STATE_GET_SELECTION)
515                                         self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
516                                         return
517                                 else:
518                                         self.v.beep(40, False)
519                                         self.vstatus.mk.set_messages(
520                                                 [(self.center('BAD PIN'), False, 1.0),
521                                                  (self.center('SORRY'), False, 0.5)])
522                                         self.vstatus.cur_user = ''
523                                         self.vstatus.cur_pin = ''
524                                 
525                                         self.reset_idler(2)
526
527                                         return
528
529         """
530         Triggered when the user presses a button while entering their user id.
531         """
532         def handle_getting_uid_key(self, event, params):
533                 key = params
534                 # complicated key handling here:
535
536                 if len(self.vstatus.cur_user) == 0 and key == 9:
537                         self.vstatus.cur_selection = ''
538                         self.vstatus.time_to_autologout = None
539                         self.vstatus.mk.set_message('PRICECHECK')
540                         sleep(0.5)
541                         self.scroll_options('', self.vstatus.mk)
542                         self.vstatus.change_state(STATE_GET_SELECTION)
543                         return
544
545                 if len(self.vstatus.cur_user) <8:
546                         if key == 11:
547                                 self.vstatus.cur_user = ''
548
549                                 self.reset_idler()
550                                 return
551                         self.vstatus.cur_user += chr(key + ord('0'))
552                         #logging.info('dob: '+vstatus.cur_user)
553                         if len(self.vstatus.cur_user) > 5:
554                                 self.vstatus.mk.set_message('>'+self.vstatus.cur_user)
555                         else:
556                                 self.vstatus.mk.set_message('UID: '+self.vstatus.cur_user)
557                 
558                 if len(self.vstatus.cur_user) == 5:
559                         uid = int(self.vstatus.cur_user)
560
561                         if uid == 0:
562                                 logging.info('user '+self.vstatus.cur_user+' has a bad PIN')
563                                 pfalken="""
564         CARRIER DETECTED
565
566         CONNECT 128000
567
568         Welcome to Picklevision Sytems, Sunnyvale, CA
569
570         Greetings Professor Falken.
571
572
573
574
575         Shall we play a game?
576
577
578         Please choose from the following menu:
579
580         1. Tic-Tac-Toe
581         2. Chess
582         3. Checkers
583         4. Backgammon
584         5. Poker
585         6. Toxic and Biochemical Warfare
586         7. Global Thermonuclear War
587
588         7 [ENTER]
589
590         Wouldn't you prefer a nice game of chess?
591
592         """.replace('\n','    ')
593                                 self.vstatus.mk.set_messages([(pfalken, False, 10)])
594                                 self.vstatus.cur_user = ''
595                                 self.vstatus.cur_pin = ''
596                                 
597                                 self.reset_idler(10)
598
599                                 return
600
601                         # TODO Fix this up do we can check before logging in
602                         """
603                         if self.dispense.isDisabled():
604                                 logging.info('user '+self.vstatus.cur_user+' is disabled')
605                                 self.vstatus.mk.set_messages(
606                                         [(' '*11+'ACCOUNT DISABLED'+' '*11, False, 3)])
607                                 self.vstatus.cur_user = ''
608                                 self.vstatus.cur_pin = ''
609                                 
610                                 self.reset_idler(3)
611                                 return
612                         """
613
614                         self.vstatus.cur_pin = ''
615                         self.vstatus.mk.set_message('PIN: ')
616                         logging.info('need pin for user %s'%self.vstatus.cur_user)
617                         self.vstatus.change_state(STATE_GETTING_PIN)
618                         return
619
620         """
621         Triggered when a key is pressed and the machine is idling.
622         """
623         def handle_idle_key(self, event, params):
624                 key = params
625                 if key == 11:
626                         self.vstatus.cur_user = ''
627                         self.reset_idler()
628                         return
629                 
630                 self.vstatus.change_state(STATE_GETTING_UID)
631                 self.run_handler(event, params)
632
633         """
634         What to do when there is nothing to do.
635         """
636         def handle_idle_tick(self, event, params):
637                 ### State idling
638                 if self.vstatus.mk.done():
639                         self.idle_step()
640
641                 if self.vstatus.time_of_next_idler and time() > self.vstatus.time_of_next_idler:
642                         self.vstatus.time_of_next_idler = time() + 30
643                         self.choose_idler()
644                 
645                 ###
646
647                 self.vstatus.mk.update_display()
648
649                 self.vstatus.change_state(STATE_GRANDFATHER_CLOCK)
650                 self.run_handler(event, params)
651                 sleep(0.05)
652
653         """
654         Manages the beeps for the grandfather clock
655         """
656         def beep_on(self, when, before=0):
657                 start = int(when - before)
658                 end = int(when)
659                 now = int(time())
660
661                 if now >= start and now <= end:
662                         return 1
663                 return 0
664
665         def handle_idle_grandfather_tick(self, event, params):
666                 ### check for interesting times
667                 now = localtime()
668
669                 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
670                 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
671                 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
672                 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
673
674                 hourfromnow = localtime(time() + 3600)
675                 
676                 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
677                 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
678                         0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
679
680                 ## check for X seconds to the hour
681                 ## if case, update counter to 2
682                 if self.beep_on(onthehour,15) \
683                         or self.beep_on(halfhour,0) \
684                         or self.beep_on(quarterhour,0) \
685                         or self.beep_on(threequarterhour,0) \
686                         or self.beep_on(fivetothehour,0):
687                         self.vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
688                         self.run_handler(event, params)
689                 else:
690                         self.vstatus.change_state(STATE_IDLE)
691
692         def handle_grandfather_tick(self, event, params):
693                 go_idle = 1
694
695                 msg = []
696                 ### we live in interesting times
697                 now = localtime()
698
699                 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
700                 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
701                 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
702                 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
703
704                 hourfromnow = localtime(time() + 3600)
705                 
706         #       onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
707                 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
708                         0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
709
710
711                 #print "when it fashionable to wear a onion on your hip"
712
713                 if self.beep_on(onthehour,15):
714                         go_idle = 0
715                         next_hour=((hourfromnow[3] + 11) % 12) + 1
716                         if onthehour - time() < next_hour and onthehour - time() > 0:
717                                 self.v.beep(0, False)
718
719                                 t = int(time())
720                                 if (t % 2) == 0:
721                                         msg.append(("DING!", False, None))
722                                 else:
723                                         msg.append(("     DING!", False, None))
724                         elif int(onthehour - time()) == 0:
725                                 self.v.beep(255, False)
726                                 msg.append(("   BONG!", False, None))
727                                 msg.append(("     IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
728                 elif self.beep_on(halfhour,0):
729                         go_idle = 0
730                         self.v.beep(0, False)
731                         msg.append((" HALFHOUR ", False, 50))
732                 elif self.beep_on(quarterhour,0):
733                         go_idle = 0
734                         self.v.beep(0, False)
735                         msg.append((" QTR HOUR ", False, 50))
736                 elif self.beep_on(threequarterhour,0):
737                         go_idle = 0
738                         self.v.beep(0, False)
739                         msg.append((" 3 QTR HR ", False, 50))
740                 elif self.beep_on(fivetothehour,0):
741                         go_idle = 0
742                         self.v.beep(0, False)
743                         msg.append(("Quick run to your lectures!  Hurry! Hurry!", False, TEXT_SPEED*4))
744                 else:
745                         go_idle = 1
746                 
747                 ## check for X seconds to the hour
748
749                 if len(msg):
750                         self.vstatus.mk.set_messages(msg)
751                         sleep(1)
752
753                 self.vstatus.mk.update_display()
754                 ## if no longer case, return to idle
755
756                 ## change idler to be clock
757                 if go_idle and self.vstatus.mk.done():
758                         self.vstatus.change_state(STATE_IDLE,1)
759
760         """
761         What to do when the door is open.
762         """
763         def handle_door_idle(self, event, params):
764                 def twiddle(clock,v,wise = 2):
765                         if (clock % 4 == 0):
766                                 v.display("-FEED  ME-")
767                         elif (clock % 4 == 1+wise):
768                                 v.display("\\FEED  ME/")
769                         elif (clock % 4 == 2):
770                                 v.display("-FEED  ME-")
771                         elif (clock % 4 == 3-wise):
772                                 v.display("/FEED  ME\\")
773
774                 # don't care right now.
775                 now = int(time())
776
777                 if ((now % 60 % 2) == 0):
778                         twiddle(now, self.v)
779                 else:
780                         twiddle(now, self.v, wise=0)
781
782         """
783         What to do when the door is opened or closed.
784         """
785         def handle_door_event(self, event, params):
786                 if params == 0:  #door open
787                         self.vstatus.change_state(STATE_DOOR_OPENING)
788                         logging.warning("Entering open door mode")
789                         self.v.display("-FEED  ME-")
790                         #door_open_mode(v);
791                         self.vstatus.cur_user = ''
792                         self.vstatus.cur_pin = ''
793                 elif params == 1:  #door closed
794                         self.vstatus.change_state(STATE_DOOR_CLOSING)
795                         self.reset_idler(3)
796
797                         logging.warning('Leaving open door mode')
798                         self.v.display("-YUM YUM!-")
799
800         """
801         Triggered when a user swipes their caed, and the machine is logged out.
802         """
803         def handle_mifare_event(self, event, params):
804                 card_id = params
805                 # Translate card_id into uid.
806                 if card_id == None or card_id == self._last_card_id:
807                         return
808
809                 self._last_card_id = card_id
810                 
811                 self.dispense.authMifareCard(card_id)
812                 logging.info('Mapped card id to uid %s'%self.dispense.getUsername())
813                 if not self.dispense.isLoggedIn():
814                         self.v.beep(40, False)
815                         self.vstatus.mk.set_messages(
816                                 [(self.center('BAD CARD'), False, 1.0),
817                                  (self.center('SORRY'), False, 0.5)])
818                         self.vstatus.cur_user = ''
819                         self.vstatus.cur_pin = ''
820                         self._last_card_id = -1
821                 
822                         self.reset_idler(2)
823                         return
824                 elif self.dispense.isDisabled():
825                         self.v.beep(40, False)
826                         self.vstatus.mk.set_messages(
827                                 [(self.center('ACCT DISABLED'), False, 1.0),
828                                  (self.center('SORRY'), False, 0.5)])
829                         self.dispense.logOut()
830                         self.reset_idler(2)
831                         return
832                 else:
833                         self.vstatus.cur_selection = ''
834                         self.vstatus.change_state(STATE_GET_SELECTION)
835                         self.scroll_options(self.vstatus.username, self.vstatus.mk, True)
836                         return
837
838         """
839         Triggered when a user swipes their card and the machine is logged in.
840         """
841         def handle_mifare_add_user_event(self, event, params):
842                 card_id = params
843
844                 # Translate card_id into uid.
845                 if card_id == None or card_id == self._last_card_id:
846                         return
847
848                 self._last_card_id = card_id
849
850                 res = self.dispense.addCard(card_id)
851
852                 if get_uid(card_id) != None:
853                         self.vstatus.mk.set_messages(
854                                 [(self.center('ALREADY'), False, 0.5),
855                                  (self.center('ENROLLED'), False, 0.5)])
856                 else:
857                         logging.info('Enrolling card %s to uid %s (%s)'%(card_id, self.vstatus.cur_user, self.vstatus.username))
858                         self.set_card_id(self.vstatus.cur_user, self.card_id)
859                         self.vstatus.mk.set_messages(
860                                 [(self.center('CARD'), False, 0.5),
861                                  (self.center('ENROLLED'), False, 0.5)])
862
863         def return_to_idle(self, event, params):
864                 self.reset_idler()
865
866         """
867         Maps what to do when the state changes.
868         """
869         def create_state_table(self):
870                 self.vstatus.state_table[(STATE_IDLE,TICK,1)] = self.handle_idle_tick
871                 self.vstatus.state_table[(STATE_IDLE,KEY,1)] = self.handle_idle_key
872                 self.vstatus.state_table[(STATE_IDLE,DOOR,1)] = self.handle_door_event
873                 self.vstatus.state_table[(STATE_IDLE,MIFARE,1)] = self.handle_mifare_event
874
875                 self.vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = self.handle_door_idle
876                 self.vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = self.handle_door_event
877                 self.vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = self.do_nothing
878                 self.vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = self.do_nothing
879
880                 self.vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = self.return_to_idle
881                 self.vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = self.handle_door_event
882                 self.vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = self.do_nothing
883                 self.vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = self.do_nothing
884
885                 self.vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = self.handle_getting_uid_idle
886                 self.vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = self.handle_door_event
887                 self.vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = self.handle_getting_uid_key
888                 self.vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = self.handle_mifare_event
889
890                 self.vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = self.handle_getting_pin_idle
891                 self.vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = self.handle_door_event
892                 self.vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = self.handle_getting_pin_key
893                 self.vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = self.handle_mifare_event
894
895                 self.vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = self.handle_get_selection_idle
896                 self.vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = self.handle_door_event
897                 self.vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = self.handle_get_selection_key
898                 self.vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = self.handle_mifare_add_user_event
899
900                 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = self.handle_idle_grandfather_tick
901                 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = self.handle_grandfather_tick
902                 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = self.handle_door_event
903                 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = self.handle_door_event
904                 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = self.do_nothing
905                 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = self.do_nothing
906                 self.vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = self.handle_mifare_event
907
908         """
909         Get what to do on a state change.
910         """
911         def get_state_table_handler(self, state, event, counter):
912                 return self.vstatus.state_table[(state,event,counter)]
913
914         def time_to_next_update(self):
915                 idle_update = self.vstatus.time_of_next_idlestep - time()
916                 if not self.vstatus.mk.done() and self.vstatus.mk.next_update is not None:
917                         mk_update = self.vstatus.mk.next_update - time()
918                         if mk_update < idle_update:
919                                 idle_update = mk_update
920                 return idle_update
921
922         def run_forever(self, rfh, wfh, options, cf):
923                 self.v = VendingMachine(rfh, wfh, USE_MIFARE)
924                 self.dispense = Dispense()
925                 self.vstatus = VendState(self.v)
926                 self.create_state_table()
927
928                 logging.debug('PING is ' + str(self.v.ping()))
929
930                 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
931
932                 self.setup_idlers()
933                 self.reset_idler()
934
935                 while True:
936                         if USE_DB:
937                                 try:
938                                         db.handle_events()
939                                 except DispenseDatabaseException, e:
940                                         logging.error('Database error: '+str(e))
941
942                         timeout = self.time_to_next_update()
943                         (event, params) = self.v.next_event(timeout)
944                         self.run_handler(event, params)
945
946         def run_handler(self, event, params):
947                 handler = self.get_state_table_handler(self.vstatus.state,event,self.vstatus.counter)
948                 if handler:
949                         handler(event, params)
950
951 """
952 Connect to the machine.
953 """
954 def connect_to_vend(options, cf):
955
956         if options.use_lat:
957                 logging.info('Connecting to vending machine using LAT')
958                 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
959                 rfh, wfh = latclient.get_fh()
960         elif options.use_serial:
961                 # Open vending machine via serial.
962                 logging.info('Connecting to vending machine using serial')
963                 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
964                 rfh,wfh = serialclient.get_fh()
965         else:
966                 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
967                 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
968                 import socket
969                 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
970                 sock.connect((options.host, options.port))
971                 rfh = sock.makefile('r')
972                 wfh = sock.makefile('w')
973                 global USE_MIFARE
974                 USE_MIFARE = 0
975                 
976         return rfh, wfh
977
978 """
979 Parse arguments from the command line
980 """
981 def parse_args():
982         from optparse import OptionParser
983
984         op = OptionParser(usage="%prog [OPTION]...")
985         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')
986         op.add_option('--serial', action='store_true', default=False, dest='use_serial', help='use the serial port')
987         op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
988         op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
989         op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
990         op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
991         op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
992         op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
993         op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
994         op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
995         op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
996         op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
997         options, args = op.parse_args()
998
999         if len(args) != 0:
1000                 op.error('extra command line arguments: ' + ' '.join(args))
1001
1002         return options
1003
1004 def create_pid_file(name):
1005         try:
1006                 pid_file = file(name, 'w')
1007                 pid_file.write('%d\n'%os.getpid())
1008                 pid_file.close()
1009         except IOError, e:
1010                 logging.warning('unable to write to pid file '+name+': '+str(e))
1011
1012 def set_stuff_up():
1013         def do_nothing(signum, stack):
1014                 signal.signal(signum, do_nothing)
1015         def stop_server(signum, stack): raise KeyboardInterrupt
1016         signal.signal(signal.SIGHUP, do_nothing)
1017         signal.signal(signal.SIGTERM, stop_server)
1018         signal.signal(signal.SIGINT, stop_server)
1019
1020         options = parse_args()
1021         config_opts = VendConfigFile(options.config_file, config_options)
1022         if options.daemon: become_daemon()
1023         set_up_logging(options)
1024         if options.pid_file != '': create_pid_file(options.pid_file)
1025
1026         return options, config_opts
1027
1028 def clean_up_nicely(options, config_opts):
1029         if options.pid_file != '':
1030                 try:
1031                         os.unlink(options.pid_file)
1032                         logging.debug('Removed pid file '+options.pid_file)
1033                 except OSError: pass  # if we can't delete it, meh
1034
1035 def set_up_logging(options):
1036         logger = logging.getLogger()
1037         
1038         if not options.daemon:
1039                 stderr_logger = logging.StreamHandler(sys.stderr)
1040                 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1041                 logger.addHandler(stderr_logger)
1042         
1043         if options.log_file != '':
1044                 try:
1045                         file_logger = logging.FileHandler(options.log_file)
1046                         file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1047                         logger.addHandler(file_logger)
1048                 except IOError, e:
1049                         logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1050
1051         if options.syslog != None:
1052                 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1053                 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1054                 logger.addHandler(sys_logger)
1055
1056         if options.quiet:
1057                 logger.setLevel(logging.WARNING)
1058         elif options.verbose:
1059                 logger.setLevel(logging.DEBUG)
1060         else:
1061                 logger.setLevel(logging.INFO)
1062
1063 def become_daemon():
1064         dev_null = file('/dev/null')
1065         fd = dev_null.fileno()
1066         os.dup2(fd, 0)
1067         os.dup2(fd, 1)
1068         os.dup2(fd, 2)
1069         try:
1070                 if os.fork() != 0:
1071                         sys.exit(0)
1072                 os.setsid()
1073         except OSError, e:
1074                 raise SystemExit('failed to fork: '+str(e))
1075
1076 def do_vend_server(options, config_opts):
1077         while True:
1078                 try:
1079                         rfh, wfh = connect_to_vend(options, config_opts)
1080                 except (SerialClientException, socket.error), e:
1081                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
1082                         del exc_traceback
1083                         logging.error("Connection error: "+str(exc_type)+" "+str(e))
1084                         logging.info("Trying again in 5 seconds.")
1085                         sleep(5)
1086                         continue
1087                 
1088 #               run_forever(rfh, wfh, options, config_opts)
1089                 
1090                 try:
1091                         vserver = VendServer()
1092                         vserver.run_forever(rfh, wfh, options, config_opts)
1093                 except VendingException:
1094                         logging.error("Connection died, trying again...")
1095                         logging.info("Trying again in 5 seconds.")
1096                         sleep(5)
1097
1098
1099 def main(argv=None):
1100         options, config_opts = set_stuff_up()
1101         while True:
1102                 try:
1103                         logging.warning('Starting Vend Server')
1104                         do_vend_server(options, config_opts)
1105                         logging.error('Vend Server finished unexpectedly, restarting')
1106                 except KeyboardInterrupt:
1107                         logging.info("Killed by signal, cleaning up")
1108                         clean_up_nicely(options, config_opts)
1109                         logging.warning("Vend Server stopped")
1110                         break
1111                 except SystemExit:
1112                         break
1113                 except:
1114                         (exc_type, exc_value, exc_traceback) = sys.exc_info()
1115                         tb = format_tb(exc_traceback, 20)
1116                         del exc_traceback
1117                         
1118                         logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1119                         logging.critical("Message: " + str(exc_value))
1120                         logging.critical("Traceback:")
1121                         for event in tb:
1122                                 for line in event.split('\n'):
1123                                         logging.critical('    '+line)
1124                         logging.critical("This message should be considered a bug in the Vend Server.")
1125                         logging.critical("Please report this to someone who can fix it.")
1126                         sleep(10)
1127                         logging.warning("Trying again anyway (might not help, but hey...)")
1128
1129 if __name__ == '__main__':
1130         sys.exit(main())

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