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

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