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

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