8 import sys, os, string, re, pwd, signal, math, syslog
9 import logging, logging.handlers
10 from traceback import format_tb
12 from time import time, sleep, mktime, localtime
13 from popen2 import popen2
14 from LATClient import LATClient, LATClientException
15 from SerialClient import SerialClient, SerialClientException
16 from VendingMachine import VendingMachine, VendingException
17 from MessageKeeper import MessageKeeper
18 from HorizScroll import HorizScroll
19 from random import random, seed
20 from Idler import GreetingIdler,TrainIdler,GrayIdler,StringIdler,ClockIdler,FortuneIdler,FileIdler,PipeIdler
21 from SnackConfig import get_snack#, get_snacks
23 from posix import geteuid
24 from LDAPConnector import get_uid, set_card_id
27 This vending machine software brought to you by:
32 and a collective of hungry alpacas.
36 For a good time call +61 8 6488 3901
58 STATE_GRANDFATHER_CLOCK,
64 class DispenseDatabaseException(Exception): pass
66 class DispenseDatabase:
67 def __init__(self, vending_machine, host, name, user, password):
68 self.vending_machine = vending_machine
69 self.db = pg.DB(dbname = name, host = host, user = user, passwd = password)
70 self.db.query('LISTEN vend_requests')
72 def process_requests(self):
73 logging.debug('database processing')
74 query = 'SELECT request_id, request_slot FROM vend_requests WHERE request_handled = false'
76 outstanding = self.db.query(query).getresult()
77 except (pg.error,), db_err:
78 raise DispenseDatabaseException('Failed to query database: %s\n'%(db_err.strip()))
79 for (id, slot) in outstanding:
80 (worked, code, string) = self.vending_machine.vend(slot)
81 logging.debug (str((worked, code, string)))
83 query = 'SELECT vend_success(%s)'%id
84 self.db.query(query).getresult()
86 query = 'SELECT vend_failed(%s)'%id
87 self.db.query(query).getresult()
89 def handle_events(self):
90 notifier = self.db.getnotify()
91 while notifier is not None:
92 self.process_requests()
93 notify = self.db.getnotify()
95 def scroll_options(username, mk, welcome = False):
97 # Balance checking: crap code, [DAA]'s fault
98 # Updated 2011 to handle new dispense [MRD]
99 raw_acct = os.popen('dispense acct %s' % username)
100 acct = raw_acct.read()
101 # this is fucking appalling
102 balance = acct[acct.find("$")+1:acct.find("(")].strip()
105 msg = [(center('WELCOME'), False, TEXT_SPEED),
106 (center(username), False, TEXT_SPEED),
107 (center(balance), False, TEXT_SPEED),]
110 choices = ' '*10+'CHOICES: '
114 for i in range(0, 7):
115 cmd = 'dispense iteminfo coke:%i' % i
119 m = re.match("\s*[a-z]+:\d+\s+(\d+)\.(\d\d)\s+([^\n]+)", info)
120 cents = int(m.group(1))*100 + int(m.group(2))
121 cokes.append('%i %i %s' % (i, cents, m.group(3)));
125 (slot_num, price, slot_name) = c.split(' ', 2)
126 if slot_name == 'dead': continue
127 choices += '%s-(%sc)-%s8 '%(slot_name, price, slot_num)
129 # we don't want to print snacks for now since it'll be too large
130 # and there's physical bits of paper in the machine anyway - matt
132 # snacks = get_snacks()
136 # for slot, ( name, price ) in snacks.items():
137 # choices += '%s8-%s (%sc) ' % ( slot, name, price )
139 choices += '55-DOOR '
140 choices += 'OR ANOTHER SNACK. '
141 choices += '99 TO READ AGAIN. '
142 choices += 'CHOICE? '
143 msg.append((choices, False, None))
146 def get_acct_state(uid):
148 info = pwd.getpwuid(uid)
150 logging.info('getting pin for uid %d: user not in password file'%uid)
152 ret = os.system('dispense acct %s' % (info.pw_name))
156 # TODO: Disabled account check (done in server pin check now)
160 def verify_user_pin(uid, pin, skip_pin_check=False):
161 info = pwd.getpwuid(uid)
163 logging.info('accepted mifare for uid %d (%s)'%(uid,info.pw_name))
164 elif os.system('dispense pincheck %04i %s' % (pin, info.pw_name)) != 0:
165 logging.info('refused pin for uid %d'%(uid))
168 logging.info('accepted pin for uid %d (%s)'%(uid,info.pw_name))
174 messages = [' WASSUP! ', 'PINK FISH ', ' SECRETS ', ' ESKIMO ', ' FORTUNES ', 'MORE MONEY']
175 choice = int(random()*len(messages))
176 msg = messages[choice]
177 left = range(len(msg))
178 for i in range(len(msg)):
179 if msg[i] == ' ': left.remove(i)
183 for i in range(0, len(msg)):
189 s += chr(int(random()*26)+ord('A'))
198 return ' '*((LEN-len(str))/2)+str
209 StringIdler(v, text="Kill 'em all", repeat=False),
210 GrayIdler(v,one="*",zero="-"),
211 StringIdler(v, text=CREDITS),
212 GrayIdler(v,one="/",zero="\\"),
214 GrayIdler(v,one="X",zero="O"),
215 FileIdler(v, '/usr/share/common-licenses/GPL-2',affinity=2),
216 GrayIdler(v,one="*",zero="-",reorder=1),
217 StringIdler(v, text=str(math.pi) + " "),
219 GrayIdler(v,one="/",zero="\\",reorder=1),
220 StringIdler(v, text=str(math.e) + " "),
221 GrayIdler(v,one="X",zero="O",reorder=1),
222 StringIdler(v, text=" I want some pizza - please call Pizza Hut Shenton Park on +61 8 9381 9979 [now closed? - MSH] - and order as Quinn - I am getting really hungry", repeat=False),
223 PipeIdler(v, "/usr/bin/getent", "passwd"),
232 def reset_idler(v, vstatus, t = None):
234 idler = GreetingIdler(v, t)
235 vstatus.time_of_next_idlestep = time()+idler.next()
236 vstatus.time_of_next_idler = None
237 vstatus.time_to_autologout = None
238 vstatus.change_state(STATE_IDLE, 1)
243 average_affinity = 10 # guessing here...
245 if idler and idler.__class__ != GreetingIdler:
246 iiindex = idlers.index(idler)
250 move = int(random()*len(idlers)*average_affinity) + 1
255 idler = idlers[iiindex]
256 move -= idler.affinity()
260 def idle_step(vstatus):
264 vstatus.time_of_next_idler = time() + 30
265 nextidle = idler.next()
267 nextidle = IDLE_SPEED
268 vstatus.time_of_next_idlestep = time()+nextidle
271 def __init__(self,v):
272 self.state_table = {}
273 self.state = STATE_IDLE
276 self.mk = MessageKeeper(v)
280 self.cur_selection = ''
281 self.time_to_autologout = None
283 self.last_timeout_refresh = None
285 def change_state(self,newstate,newcounter=None):
286 if self.state != newstate:
287 #print "Changing state from: ",
291 self.state = newstate
293 if newcounter is not None and self.counter != newcounter:
294 #print "Changing counter from: ",
298 self.counter = newcounter
302 def handle_tick_event(event, params, v, vstatus):
303 # don't care right now.
306 def handle_switch_event(event, params, v, vstatus):
307 # don't care right now.
311 def do_nothing(state, event, params, v, vstatus):
312 print "doing nothing (s,e,p)", state, " ", event, " ", params
315 def handle_getting_uid_idle(state, event, params, v, vstatus):
316 # don't care right now.
319 def handle_getting_pin_idle(state, event, params, v, vstatus):
320 # don't care right now.
323 def handle_get_selection_idle(state, event, params, v, vstatus):
324 # don't care right now.
326 ### State logging out ..
327 if vstatus.time_to_autologout != None:
328 time_left = vstatus.time_to_autologout - time()
329 if time_left < 6 and (vstatus.last_timeout_refresh is None or vstatus.last_timeout_refresh > time_left):
330 vstatus.mk.set_message('LOGOUT: '+str(int(time_left)))
331 vstatus.last_timeout_refresh = int(time_left)
332 vstatus.cur_selection = ''
334 if vstatus.time_to_autologout != None and vstatus.time_to_autologout - time() <= 0:
335 vstatus.time_to_autologout = None
336 vstatus.cur_user = ''
338 vstatus.cur_selection = ''
340 reset_idler(v, vstatus)
342 ### State fully logged out ... reset variables
343 if vstatus.time_to_autologout and not vstatus.mk.done():
344 vstatus.time_to_autologout = None
345 if vstatus.cur_user == '' and vstatus.time_to_autologout:
346 vstatus.time_to_autologout = None
349 if len(vstatus.cur_pin) == PIN_LENGTH and vstatus.mk.done() and vstatus.time_to_autologout == None:
351 vstatus.time_to_autologout = time() + 15
352 vstatus.last_timeout_refresh = None
354 ## FIXME - this may need to be elsewhere.....
356 vstatus.mk.update_display()
360 def handle_get_selection_key(state, event, params, v, vstatus):
362 if len(vstatus.cur_selection) == 0:
365 vstatus.cur_user = ''
366 vstatus.cur_selection = ''
368 vstatus.mk.set_messages([(center('BYE!'), False, 1.5)])
369 reset_idler(v, vstatus, 2)
371 vstatus.cur_selection += chr(key + ord('0'))
372 vstatus.mk.set_message('SELECT: '+vstatus.cur_selection)
373 vstatus.time_to_autologout = None
374 elif len(vstatus.cur_selection) == 1:
376 vstatus.cur_selection = ''
377 vstatus.time_to_autologout = None
378 scroll_options(vstatus.username, vstatus.mk)
381 vstatus.cur_selection += chr(key + ord('0'))
383 make_selection(v,vstatus)
384 vstatus.cur_selection = ''
385 vstatus.time_to_autologout = time() + 8
386 vstatus.last_timeout_refresh = None
389 price_check(v,vstatus)
390 vstatus.cur_selection = ''
391 vstatus.time_to_autologout = None
392 vstatus.last_timeout_refresh = None
394 def make_selection(v, vstatus):
395 # should use sudo here
396 if vstatus.cur_selection == '55':
397 vstatus.mk.set_message('OPENSESAME')
398 logging.info('dispensing a door for %s'%vstatus.username)
400 #ret = os.system('su - "%s" -c "dispense door"'%vstatus.username)
401 ret = os.system('dispense -u "%s" door'%vstatus.username)
403 ret = os.system('dispense door')
405 logging.info('door opened')
406 vstatus.mk.set_message(center('DOOR OPEN'))
408 logging.warning('user %s tried to dispense a bad door'%vstatus.username)
409 vstatus.mk.set_message(center('BAD DOOR'))
411 elif vstatus.cur_selection == '81':
413 elif vstatus.cur_selection == '99':
414 scroll_options(vstatus.username, vstatus.mk)
415 vstatus.cur_selection = ''
417 elif vstatus.cur_selection[1] == '8':
418 v.display('GOT DRINK?')
419 if ((os.system('dispense -u "%s" coke:%s'%(vstatus.username, vstatus.cur_selection[0])) >> 8) != 0):
420 v.display('SEEMS NOT')
422 v.display('GOT DRINK!')
423 #v.display('SEE FRIDGE')
425 # first see if it's a named slot
427 price, shortname, name = get_snack( vstatus.cur_selection )
429 price, shortname, name = get_snack( '--' )
430 dollarprice = "$%.2f" % ( price / 100.0 )
431 v.display(vstatus.cur_selection+' - %s'%dollarprice)
432 exitcode = os.system('dispense -u "%s" give \>sales\:snack %d "%s"'%(vstatus.username, price, name)) >> 8
433 # For some reason, this causes the machine and this code to desync
434 # exitcode = os.system('dispense -u "%s" snack:%s'%(vstatus.username, vstatus.cur_selection)) >> 8
436 # magic dispense syslog service
437 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "vended %s (slot %s) for %s" % (name, vstatus.cur_selection, vstatus.username))
438 v.vend(vstatus.cur_selection)
439 v.display('THANK YOU')
440 elif (exitcode == 5): # RV_BALANCE
441 v.display('NO MONEY?')
442 elif (exitcode == 4): # RV_ARGUMENTS (zero give causes arguments)
443 v.display('EMPTY SLOT')
444 elif (exitcode == 1): # RV_BADITEM (Dead slot)
445 v.display('EMPTY SLOT')
447 syslog.syslog(syslog.LOG_INFO | syslog.LOG_LOCAL4, "failed vending %s (slot %s) for %s (code %d)" % (name, vstatus.cur_selection, vstatus.username, exitcode))
448 v.display('UNK ERROR')
452 def price_check(v, vstatus):
453 if vstatus.cur_selection[1] == '8':
454 v.display(center('SEE COKE'))
456 # first see if it's a named slot
458 price, shortname, name = get_snack( vstatus.cur_selection )
460 price, shortname, name = get_snack( '--' )
461 dollarprice = "$%.2f" % ( price / 100.0 )
462 v.display(vstatus.cur_selection+' - %s'%dollarprice)
465 def handle_getting_pin_key(state, event, params, v, vstatus):
466 #print "handle_getting_pin_key (s,e,p)", state, " ", event, " ", params
468 if len(vstatus.cur_pin) < PIN_LENGTH:
470 if vstatus.cur_pin == '':
471 vstatus.cur_user = ''
472 reset_idler(v, vstatus)
476 vstatus.mk.set_message('PIN: ')
478 vstatus.cur_pin += chr(key + ord('0'))
479 vstatus.mk.set_message('PIN: '+'X'*len(vstatus.cur_pin))
480 if len(vstatus.cur_pin) == PIN_LENGTH:
481 vstatus.username = verify_user_pin(int(vstatus.cur_user), int(vstatus.cur_pin))
484 vstatus.cur_selection = ''
485 vstatus.change_state(STATE_GET_SELECTION)
486 scroll_options(vstatus.username, vstatus.mk, True)
490 vstatus.mk.set_messages(
491 [(center('BAD PIN'), False, 1.0),
492 (center('SORRY'), False, 0.5)])
493 vstatus.cur_user = ''
496 reset_idler(v, vstatus, 2)
501 def handle_getting_uid_key(state, event, params, v, vstatus):
502 #print "handle_getting_uid_key (s,e,p)", state, " ", event, " ", params
505 # complicated key handling here:
507 if len(vstatus.cur_user) == 0 and key == 9:
508 vstatus.cur_selection = ''
509 vstatus.time_to_autologout = None
510 vstatus.mk.set_message('PRICECHECK')
512 scroll_options('', vstatus.mk)
513 vstatus.change_state(STATE_GET_SELECTION)
516 if len(vstatus.cur_user) <8:
518 vstatus.cur_user = ''
520 reset_idler(v, vstatus)
522 vstatus.cur_user += chr(key + ord('0'))
523 #logging.info('dob: '+vstatus.cur_user)
524 if len(vstatus.cur_user) > 5:
525 vstatus.mk.set_message('>'+vstatus.cur_user)
527 vstatus.mk.set_message('UID: '+vstatus.cur_user)
529 if len(vstatus.cur_user) == 5:
530 uid = int(vstatus.cur_user)
533 logging.info('user '+vstatus.cur_user+' has a bad PIN')
539 Welcome to Picklevision Sytems, Sunnyvale, CA
541 Greetings Professor Falken.
546 Shall we play a game?
549 Please choose from the following menu:
556 6. Toxic and Biochemical Warfare
557 7. Global Thermonuclear War
561 Wouldn't you prefer a nice game of chess?
563 """.replace('\n',' ')
564 vstatus.mk.set_messages([(pfalken, False, 10)])
565 vstatus.cur_user = ''
568 reset_idler(v, vstatus, 10)
572 acct_state = get_acct_state(uid)
573 if acct_state == 'invalid':
574 logging.info('user '+vstatus.cur_user+' is not in the database')
575 vstatus.mk.set_messages(
576 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
577 vstatus.cur_user = ''
580 reset_idler(v, vstatus, 3)
582 elif acct_state == 'locked':
583 logging.info('user '+vstatus.cur_user+' is locked')
584 vstatus.mk.set_messages(
585 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
586 vstatus.cur_user = ''
589 reset_idler(v, vstatus, 3)
591 elif acct_state == 'good':
593 vstatus.mk.set_message('PIN: ')
594 logging.info('need pin for user %s'%vstatus.cur_user)
595 vstatus.change_state(STATE_GETTING_PIN)
598 logging.error('user '+vstatus.cur_user+' has an unknown account state'+acct_state)
599 vstatus.mk.set_messages(
600 [(' '*10+'INVALID PIN SETUP'+' '*11, False, 3)])
601 vstatus.cur_user = ''
604 reset_idler(v, vstatus, 3)
608 def handle_idle_key(state, event, params, v, vstatus):
609 #print "handle_idle_key (s,e,p)", state, " ", event, " ", params
614 vstatus.cur_user = ''
615 reset_idler(v, vstatus)
618 vstatus.change_state(STATE_GETTING_UID)
619 run_handler(event, key, v, vstatus)
622 def handle_idle_tick(state, event, params, v, vstatus):
624 if vstatus.mk.done():
627 if vstatus.time_of_next_idler and time() > vstatus.time_of_next_idler:
628 vstatus.time_of_next_idler = time() + 30
633 vstatus.mk.update_display()
635 vstatus.change_state(STATE_GRANDFATHER_CLOCK)
636 run_handler(event, params, v, vstatus)
639 def beep_on(when, before=0):
640 start = int(when - before)
644 if now >= start and now <= end:
648 def handle_idle_grandfather_tick(state, event, params, v, vstatus):
649 ### check for interesting times
652 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
653 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
654 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
655 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
657 hourfromnow = localtime(time() + 3600)
659 #onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
660 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
661 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
663 ## check for X seconds to the hour
664 ## if case, update counter to 2
665 if beep_on(onthehour,15) \
666 or beep_on(halfhour,0) \
667 or beep_on(quarterhour,0) \
668 or beep_on(threequarterhour,0) \
669 or beep_on(fivetothehour,0):
670 vstatus.change_state(STATE_GRANDFATHER_CLOCK,2)
671 run_handler(event, params, v, vstatus)
673 vstatus.change_state(STATE_IDLE)
675 def handle_grandfather_tick(state, event, params, v, vstatus):
679 ### we live in interesting times
682 quarterhour = mktime([now[0],now[1],now[2],now[3],15,0,now[6],now[7],now[8]])
683 halfhour = mktime([now[0],now[1],now[2],now[3],30,0,now[6],now[7],now[8]])
684 threequarterhour = mktime([now[0],now[1],now[2],now[3],45,0,now[6],now[7],now[8]])
685 fivetothehour = mktime([now[0],now[1],now[2],now[3],55,0,now[6],now[7],now[8]])
687 hourfromnow = localtime(time() + 3600)
689 # onthehour = mktime([now[0],now[1],now[2],now[3],03,0,now[6],now[7],now[8]])
690 onthehour = mktime([hourfromnow[0],hourfromnow[1],hourfromnow[2],hourfromnow[3], \
691 0,0,hourfromnow[6],hourfromnow[7],hourfromnow[8]])
694 #print "when it fashionable to wear a onion on your hip"
696 if beep_on(onthehour,15):
698 next_hour=((hourfromnow[3] + 11) % 12) + 1
699 if onthehour - time() < next_hour and onthehour - time() > 0:
704 msg.append(("DING!", False, None))
706 msg.append((" DING!", False, None))
707 elif int(onthehour - time()) == 0:
709 msg.append((" BONG!", False, None))
710 msg.append((" IT'S "+ str(next_hour) + "O'CLOCK AND ALL IS WELL .....", False, TEXT_SPEED*4))
711 elif beep_on(halfhour,0):
714 msg.append((" HALFHOUR ", False, 50))
715 elif beep_on(quarterhour,0):
718 msg.append((" QTR HOUR ", False, 50))
719 elif beep_on(threequarterhour,0):
722 msg.append((" 3 QTR HR ", False, 50))
723 elif beep_on(fivetothehour,0):
726 msg.append(("Quick run to your lectures! Hurry! Hurry!", False, TEXT_SPEED*4))
730 ## check for X seconds to the hour
733 vstatus.mk.set_messages(msg)
736 vstatus.mk.update_display()
737 ## if no longer case, return to idle
739 ## change idler to be clock
740 if go_idle and vstatus.mk.done():
741 vstatus.change_state(STATE_IDLE,1)
743 def handle_door_idle(state, event, params, v, vstatus):
744 def twiddle(clock,v,wise = 2):
746 v.display("-FEED ME-")
747 elif (clock % 4 == 1+wise):
748 v.display("\\FEED ME/")
749 elif (clock % 4 == 2):
750 v.display("-FEED ME-")
751 elif (clock % 4 == 3-wise):
752 v.display("/FEED ME\\")
754 # don't care right now.
757 if ((now % 60 % 2) == 0):
760 twiddle(now, v, wise=0)
763 def handle_door_event(state, event, params, v, vstatus):
764 if params == 0: #door open
765 vstatus.change_state(STATE_DOOR_OPENING)
766 logging.warning("Entering open door mode")
767 v.display("-FEED ME-")
769 vstatus.cur_user = ''
771 elif params == 1: #door closed
772 vstatus.change_state(STATE_DOOR_CLOSING)
773 reset_idler(v, vstatus, 3)
775 logging.warning('Leaving open door mode')
776 v.display("-YUM YUM!-")
778 def handle_mifare_event(state, event, params, v, vstatus):
780 # Translate card_id into uid.
785 vstatus.cur_user = get_uid(card_id)
786 logging.info('Mapped card id to uid %s'%vstatus.cur_user)
787 vstatus.username = verify_user_pin(int(vstatus.cur_user), None, True)
789 vstatus.username = None
793 vstatus.cur_selection = ''
794 vstatus.change_state(STATE_GET_SELECTION)
795 scroll_options(vstatus.username, vstatus.mk, True)
799 vstatus.mk.set_messages(
800 [(center('BAD CARD'), False, 1.0),
801 (center('SORRY'), False, 0.5)])
802 vstatus.cur_user = ''
805 reset_idler(v, vstatus, 2)
808 def handle_mifare_add_user_event(state, event, params, v, vstatus):
811 # Translate card_id into uid.
816 if get_uid(card_id) != None:
817 vstatus.mk.set_messages(
818 [(center('ALREADY'), False, 0.5),
819 (center('ENROLLED'), False, 0.5)])
821 # scroll_options(vstatus.username, vstatus.mk)
826 logging.info('Enrolling card %s to uid %s (%s)'%(card_id, vstatus.cur_user, vstatus.username))
827 set_card_id(vstatus.cur_user, card_id)
828 vstatus.mk.set_messages(
829 [(center('CARD'), False, 0.5),
830 (center('ENROLLED'), False, 0.5)])
832 # scroll_options(vstatus.username, vstatus.mk)
834 def return_to_idle(state,event,params,v,vstatus):
835 reset_idler(v, vstatus)
837 def create_state_table(vstatus):
838 vstatus.state_table[(STATE_IDLE,TICK,1)] = handle_idle_tick
839 vstatus.state_table[(STATE_IDLE,KEY,1)] = handle_idle_key
840 vstatus.state_table[(STATE_IDLE,DOOR,1)] = handle_door_event
841 vstatus.state_table[(STATE_IDLE,MIFARE,1)] = handle_mifare_event
843 vstatus.state_table[(STATE_DOOR_OPENING,TICK,1)] = handle_door_idle
844 vstatus.state_table[(STATE_DOOR_OPENING,DOOR,1)] = handle_door_event
845 vstatus.state_table[(STATE_DOOR_OPENING,KEY,1)] = do_nothing
846 vstatus.state_table[(STATE_DOOR_OPENING,MIFARE,1)] = do_nothing
848 vstatus.state_table[(STATE_DOOR_CLOSING,TICK,1)] = return_to_idle
849 vstatus.state_table[(STATE_DOOR_CLOSING,DOOR,1)] = handle_door_event
850 vstatus.state_table[(STATE_DOOR_CLOSING,KEY,1)] = do_nothing
851 vstatus.state_table[(STATE_DOOR_CLOSING,MIFARE,1)] = do_nothing
853 vstatus.state_table[(STATE_GETTING_UID,TICK,1)] = handle_getting_uid_idle
854 vstatus.state_table[(STATE_GETTING_UID,DOOR,1)] = do_nothing
855 vstatus.state_table[(STATE_GETTING_UID,KEY,1)] = handle_getting_uid_key
856 vstatus.state_table[(STATE_GETTING_UID,MIFARE,1)] = handle_mifare_event
858 vstatus.state_table[(STATE_GETTING_PIN,TICK,1)] = handle_getting_pin_idle
859 vstatus.state_table[(STATE_GETTING_PIN,DOOR,1)] = do_nothing
860 vstatus.state_table[(STATE_GETTING_PIN,KEY,1)] = handle_getting_pin_key
861 vstatus.state_table[(STATE_GETTING_PIN,MIFARE,1)] = handle_mifare_event
863 vstatus.state_table[(STATE_GET_SELECTION,TICK,1)] = handle_get_selection_idle
864 vstatus.state_table[(STATE_GET_SELECTION,DOOR,1)] = do_nothing
865 vstatus.state_table[(STATE_GET_SELECTION,KEY,1)] = handle_get_selection_key
866 vstatus.state_table[(STATE_GET_SELECTION,MIFARE,1)] = handle_mifare_add_user_event
868 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,1)] = handle_idle_grandfather_tick
869 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,TICK,2)] = handle_grandfather_tick
870 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,1)] = do_nothing
871 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,DOOR,2)] = do_nothing
872 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,1)] = do_nothing
873 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,KEY,2)] = do_nothing
874 vstatus.state_table[(STATE_GRANDFATHER_CLOCK,MIFARE,1)] = handle_mifare_event
876 def get_state_table_handler(vstatus, state, event, counter):
877 return vstatus.state_table[(state,event,counter)]
879 def time_to_next_update(vstatus):
880 idle_update = vstatus.time_of_next_idlestep - time()
881 if not vstatus.mk.done() and vstatus.mk.next_update is not None:
882 mk_update = vstatus.mk.next_update - time()
883 if mk_update < idle_update:
884 idle_update = mk_update
887 def run_forever(rfh, wfh, options, cf):
888 v = VendingMachine(rfh, wfh, USE_MIFARE)
889 vstatus = VendState(v)
890 create_state_table(vstatus)
892 logging.debug('PING is ' + str(v.ping()))
894 if USE_DB: db = DispenseDatabase(v, cf.DBServer, cf.DBName, cf.DBUser, cf.DBPassword)
897 reset_idler(v, vstatus)
899 # This main loop was hideous and the work of the devil.
900 # This has now been fixed (mostly) - mtearle
903 # notes for later surgery
904 # (event, counter, ' ')
908 # ( return state - not currently implemented )
914 except DispenseDatabaseException, e:
915 logging.error('Database error: '+str(e))
917 timeout = time_to_next_update(vstatus)
918 e = v.next_event(timeout)
921 run_handler(event, params, v, vstatus)
923 # logging.debug('Got event: ' + repr(e))
926 def run_handler(event, params, v, vstatus):
927 handler = get_state_table_handler(vstatus,vstatus.state,event,vstatus.counter)
929 handler(vstatus.state, event, params, v, vstatus)
931 def connect_to_vend(options, cf):
934 logging.info('Connecting to vending machine using LAT')
935 latclient = LATClient(service = cf.ServiceName, password = cf.ServicePassword, server_name = cf.ServerName, connect_password = cf.ConnectPassword, priv_password = cf.PrivPassword)
936 rfh, wfh = latclient.get_fh()
937 elif options.use_serial:
938 # Open vending machine via serial.
939 logging.info('Connecting to vending machine using serial')
940 serialclient = SerialClient(port = '/dev/ttyS1', baud = 9600)
941 rfh,wfh = serialclient.get_fh()
943 #(rfh, wfh) = popen2('../../virtualvend/vvend.py')
944 logging.info('Connecting to virtual vending machine on %s:%d'%(options.host,options.port))
946 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
947 sock.connect((options.host, options.port))
948 rfh = sock.makefile('r')
949 wfh = sock.makefile('w')
956 from optparse import OptionParser
958 op = OptionParser(usage="%prog [OPTION]...")
959 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')
960 op.add_option('--serial', action='store_true', default=True, dest='use_serial', help='use the serial port')
961 op.add_option('--lat', action='store_true', default=False, dest='use_lat', help='use LAT')
962 op.add_option('--virtualvend', action='store_false', default=True, dest='use_serial', help='use the virtual vending server instead of LAT')
963 op.add_option('-n', '--hostname', dest='host', default='localhost', help='the hostname to connect to for virtual vending machine mode (default: localhost)')
964 op.add_option('-p', '--port', dest='port', default=5150, type='int', help='the port number to connect to (default: 5150)')
965 op.add_option('-l', '--log-file', metavar='FILE', dest='log_file', default='', help='log output to the specified file')
966 op.add_option('-s', '--syslog', dest='syslog', metavar='FACILITY', default=None, help='log output to given syslog facility')
967 op.add_option('-d', '--daemon', dest='daemon', action='store_true', default=False, help='run as a daemon')
968 op.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='spit out lots of debug output')
969 op.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='only report errors')
970 op.add_option('--pid-file', dest='pid_file', metavar='FILE', default='', help='store daemon\'s pid in the given file')
971 options, args = op.parse_args()
974 op.error('extra command line arguments: ' + ' '.join(args))
979 'DBServer': ('Database', 'Server'),
980 'DBName': ('Database', 'Name'),
981 'DBUser': ('VendingMachine', 'DBUser'),
982 'DBPassword': ('VendingMachine', 'DBPassword'),
984 'ServiceName': ('VendingMachine', 'ServiceName'),
985 'ServicePassword': ('VendingMachine', 'Password'),
987 'ServerName': ('DecServer', 'Name'),
988 'ConnectPassword': ('DecServer', 'ConnectPassword'),
989 'PrivPassword': ('DecServer', 'PrivPassword'),
992 class VendConfigFile:
993 def __init__(self, config_file, options):
995 cp = ConfigParser.ConfigParser()
998 for option in options:
999 section, name = options[option]
1000 value = cp.get(section, name)
1001 self.__dict__[option] = value
1003 except ConfigParser.Error, e:
1004 raise SystemExit("Error reading config file "+config_file+": " + str(e))
1006 def create_pid_file(name):
1008 pid_file = file(name, 'w')
1009 pid_file.write('%d\n'%os.getpid())
1012 logging.warning('unable to write to pid file '+name+': '+str(e))
1015 def do_nothing(signum, stack):
1016 signal.signal(signum, do_nothing)
1017 def stop_server(signum, stack): raise KeyboardInterrupt
1018 signal.signal(signal.SIGHUP, do_nothing)
1019 signal.signal(signal.SIGTERM, stop_server)
1020 signal.signal(signal.SIGINT, stop_server)
1022 options = parse_args()
1023 config_opts = VendConfigFile(options.config_file, config_options)
1024 if options.daemon: become_daemon()
1025 set_up_logging(options)
1026 if options.pid_file != '': create_pid_file(options.pid_file)
1028 return options, config_opts
1030 def clean_up_nicely(options, config_opts):
1031 if options.pid_file != '':
1033 os.unlink(options.pid_file)
1034 logging.debug('Removed pid file '+options.pid_file)
1035 except OSError: pass # if we can't delete it, meh
1037 def set_up_logging(options):
1038 logger = logging.getLogger()
1040 if not options.daemon:
1041 stderr_logger = logging.StreamHandler(sys.stderr)
1042 stderr_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
1043 logger.addHandler(stderr_logger)
1045 if options.log_file != '':
1047 file_logger = logging.FileHandler(options.log_file)
1048 file_logger.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s'))
1049 logger.addHandler(file_logger)
1051 logger.warning('unable to write to log file '+options.log_file+': '+str(e))
1053 if options.syslog != None:
1054 sys_logger = logging.handlers.SysLogHandler('/dev/log', options.syslog)
1055 sys_logger.setFormatter(logging.Formatter('vendserver[%d]'%(os.getpid()) + ' %(levelname)s: %(message)s'))
1056 logger.addHandler(sys_logger)
1059 logger.setLevel(logging.WARNING)
1060 elif options.verbose:
1061 logger.setLevel(logging.DEBUG)
1063 logger.setLevel(logging.INFO)
1065 def become_daemon():
1066 dev_null = file('/dev/null')
1067 fd = dev_null.fileno()
1076 raise SystemExit('failed to fork: '+str(e))
1078 def do_vend_server(options, config_opts):
1081 rfh, wfh = connect_to_vend(options, config_opts)
1082 except (SerialClientException, socket.error), e:
1083 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1085 logging.error("Connection error: "+str(exc_type)+" "+str(e))
1086 logging.info("Trying again in 5 seconds.")
1090 # run_forever(rfh, wfh, options, config_opts)
1093 run_forever(rfh, wfh, options, config_opts)
1094 except VendingException:
1095 logging.error("Connection died, trying again...")
1096 logging.info("Trying again in 5 seconds.")
1099 if __name__ == '__main__':
1100 options, config_opts = set_stuff_up()
1103 logging.warning('Starting Vend Server')
1104 do_vend_server(options, config_opts)
1105 logging.error('Vend Server finished unexpectedly, restarting')
1106 except KeyboardInterrupt:
1107 logging.info("Killed by signal, cleaning up")
1108 clean_up_nicely(options, config_opts)
1109 logging.warning("Vend Server stopped")
1114 (exc_type, exc_value, exc_traceback) = sys.exc_info()
1115 tb = format_tb(exc_traceback, 20)
1118 logging.critical("Uh-oh, unhandled " + str(exc_type) + " exception")
1119 logging.critical("Message: " + str(exc_value))
1120 logging.critical("Traceback:")
1122 for line in event.split('\n'):
1123 logging.critical(' '+line)
1124 logging.critical("This message should be considered a bug in the Vend Server.")
1125 logging.critical("Please report this to someone who can fix it.")
1127 logging.warning("Trying again anyway (might not help, but hey...)")