class PortalBoxApplication: ''' wrap code as a class to allow for clean sharing of objects between states ''' def __init__(self, settings): ''' Setup the bare minimun, defering as much as poosible to the run method so signal handlers can be configured in __main__ ''' self.running = False self.equipment_id = False self.box = PortalBox() self.settings = settings self.start_time = 0.0 feed_watchdog("portalbox_init") def __del__(self): ''' free resources after run ''' self.box.cleanup() def run(self): ''' Actually get ready to run... we defered initialization in order to configure signal handlers in __main__ but they should now be in place This corresponds to the transition from Start in FSM.odg see docs ''' # Step 1 Do a bit of a dance to show we are running logging.info("Setting display color to wipe red") self.box.set_display_color_wipe(RED, 10) logging.info("Started PortalBoxApplication.run()") # Set 2 Figure out our identity mac_address = format(get_mac_address(), 'x') logging.info("Discovered Mac Address: %s", mac_address) # connect to backend database logging.info("Connecting to database on host %s", self.settings['db']['host']) try: logging.debug("Creating database instance") self.db = Database(self.settings['db']) logging.info("Connected to Database") except Exception as e: logging.error("{}".format(e)) sys.exit(1) # be prepared to send emails try: logging.info("Creating emailer instance") self.emailer = Emailer(self.settings['email']) logging.info("Cached email settings") except Exception as e: # should be unreachable logging.error("{}".format(e)) sys.exit(1) # give user hint we are makeing progress logging.debug("Setting display color to wipe orange") self.box.set_display_color_wipe(ORANGE, 10) # determine what we are profile = (-1,) self.running = True while self.running and 0 > profile[0]: feed_watchdog("equipment_profile") logging.info("Trying to get equipment profile") profile = self.db.get_equipment_profile(mac_address) if 0 > profile[0]: sleep(5) # only run if we have role, which we might not if systemd asked us to # shutdown before we discovered a role if 0 < profile[0]: # profile: # (int) equipment id # (int) equipment type id # (str) equipment type # (int) location id # (str) location # (int) time limit in minutes self.equipment_id = profile[0] self.equipment_type_id = profile[1] self.equipment_type = profile[2] self.location = profile[4] self.timeout_period = profile[5] logging.info("Discovered identity. Type: %s(%s) Timeout: %s m", self.equipment_type, self.equipment_type_id, self.timeout_period) self.db.log_started_status(self.equipment_id) logging.info("Setting display to wipe green") self.box.set_display_color_wipe(GREEN, 10) self.timeout_period *= 60 # python threading wants seconds, DB has minutes self.proxy_uid = -1 self.training_mode = False logging.info("Starting to wait for access card") self.wait_for_access_card() else: logging.info("Running ending; did not discover identity.") sys.exit(1) def wait_for_access_card(self): ''' Wait for a card and if we read a card decide what to do ''' logging.debug("Setting display to sleep") self.box.sleep_display() # Run... loop endlessly waiting for RFID cards logging.debug("Waiting for an access card") while self.running: feed_watchdog("wait_for_a_card") # Scan for card uid = self.box.read_RFID_card() if -1 < uid: logging.debug("Detected a card") # we read a card... decide what to do card_type = self.db.get_card_type(uid) logging.debug("Card of type: %s was presented", card_type) if Database.SHUTDOWN_CARD == card_type: logging.info("Shutdown Card: %s detected, triggering box shutdown", uid) self.db.log_shutdown_status(self.equipment_id, uid) logging.debug("Blanking display") self.box.set_display_color() logging.debug("Telling OS to shut down") os.system("sync; shutdown -h now") elif Database.USER_CARD == card_type: logging.info("User card %s detected, authorized?", uid) if self.db.is_user_authorized_for_equipment_type(uid, self.equipment_type_id): logging.info("User %s authorized for %s", uid, self.equipment_type) self.run_session(uid) else: self.wait_for_unauthorized_card_removal(uid) logging.debug("Done with user card, start sleep display") self.box.sleep_display() else: logging.info("Unauthorized card %s detected", uid) self.wait_for_unauthorized_card_removal(uid) logging.debug("Done with unauthorized card, start sleep display") self.box.sleep_display() self.box.sleep_display() sleep(0.1) def run_session(self, user_id): ''' Allow user to use the equipment ''' self.authorized_uid = user_id self.proxy_uid = -1 self.training_mode = False self.user_is_trainer = False logging.debug("Setting display to green") self.box.set_display_color(AUTH_COLOR) self.box.set_buzzer(True) self.box.set_equipment_power_on(True) sleep(0.05) logging.info("Logging activation of %s to DB", self.equipment_type) self.db.log_access_attempt(user_id, self.equipment_id, True) self.box.set_buzzer(False) logging.debug("Setting display to green") self.box.set_display_color(AUTH_COLOR) logging.debug("Checking if user is a trainer or admin") self.user_is_trainer = self.db.is_user_trainer(user_id) if 0 < self.timeout_period: self.start_time = time() logging.debug("Starting timeout") self.wait_for_authorized_card_removal() self.box.set_equipment_power_on(False) logging.info("Logging end of %s access to DB", self.equipment_type) self.db.log_access_completion(user_id, self.equipment_id) self.authorized_uid = -1 logging.debug("run_session() ends") def wait_for_unauthorized_card_removal(self, uid): ''' Wait for card to be removed ''' logging.info("Card %s NOT authorized for %s", uid, self.equipment_type) self.db.log_access_attempt(uid, self.equipment_id, False) #loop endlessly waiting for shutdown or card to be removed logging.debug("Looping until not running or card removed") self.box.flash_display(RED, 100, 1000, RED) while self.running: feed_watchdog("wait_unauth_remove") # Scan for card uid = self.box.read_RFID_card() if uid < 0: # card removed break sleep(0.1) logging.debug("wait_for_unauthorized_card_removal() ends") def wait_for_authorized_card_removal(self): ''' Wait for card to be removed ''' self.card_present = True self.proxy_uid = -1 if self.training_mode: color_now = TRAINER_COLOR else: color_now = AUTH_COLOR #loop endlessly waiting for shutdown or card to be removed logging.debug("Waiting for card removal or timeout") while self.running and self.card_present: feed_watchdog("wait_auth_remove_timeout") # check for timeout if (self.timeout_period > 0 and (time() - self.start_time) > self.timeout_period): logging.debug("Time exceeded, wait for timeout grace") self.wait_for_timeout_grace_period_to_expire() logging.debug("Timeout grace period expired") if self.card_present: # User pressed the button return to running logging.debug("Button pressed, restart timeout") self.start_time = time() if self.proxy_uid > -1: color_now = PROXY_COLOR elif self.training_mode: color_now = TRAINER_COLOR else: color_now = AUTH_COLOR else: logging.debug("Card removed") break # Scan for card uid = self.box.read_RFID_card() # FIXME Accept training card also? if -1 < uid and (uid == self.authorized_uid or uid == self.proxy_uid): pass else: # we did not read a card or we read the wrong card self.wait_for_user_card_return() if -1 < self.proxy_uid: color_now = PROXY_COLOR elif self.training_mode: color_now = TRAINER_COLOR else: color_now = AUTH_COLOR self.box.set_display_color(color_now) sleep(0.1) self.box.set_display_color(color_now) logging.debug("Finished waiting for card removal or timeout") def wait_for_user_card_return(self): ''' Wait for a time for card to return before shutting down, button press shuts down immediately. We accomplish this using the card_present flag. By setting the flag to False immeditely we just return and the outer loop in wait_for_authorized_card_removal will also end. If we get the authorized card back we can toggle the flag back and return which will cause the outer loop to continue ''' logging.info("User card removed") self.card_present = False self.proxy_uid = -1 grace_seconds = 10 grace_start_time = time() logging.debug("Setting display to yellow") #self.box.set_display_color(YELLOW) self.box.flash_display(YELLOW, 200, (grace_seconds * 5000), YELLOW) self.box.has_button_been_pressed() # clear pending events logging.debug("Waiting for card to return") previous_uid = -1 loop_count = 0 while (self.running and (time() - grace_start_time) < grace_seconds): feed_watchdog("wait_auth_card_return") # Check for button press if self.box.has_button_been_pressed(): logging.debug("Button pressed") break # Scan for card uid = self.box.read_RFID_card() if uid > -1 and uid != previous_uid: # we read a card previous_uid = uid if uid == self.authorized_uid: # card returned self.card_present = True logging.debug("Authorized card returned") break elif not self.training_mode: # trainers may not use proxy cards logging.debug("Checking database for card type") card_type = self.db.get_card_type(uid) if Database.PROXY_CARD == card_type: self.card_present = True self.proxy_uid = uid self.user_is_trainer = False logging.debug("Authorized user -> proxy card") break if Database.TRAINING_CARD == card_type: logging.info("Training card %s detected, authorized?", uid) if self.proxy_uid > -1: logging.info("Training card disallowed with proxy") elif not self.user_is_trainer: logging.info("User is not a trainer") elif self.db.is_training_card_for_equipment_type(uid, self.equipment_type_id): logging.info("Training card %s authorized", uid) self.db.log_access_attempt(uid, self.equipment_id, True) self.card_present = True self.training_mode = True self.user_is_trainer = False self.authorized_uid = uid break else: logging.info("Training card NOT authorized for %s", uid, self.equipment_type) if (loop_count % 20) == 0: self.box.set_buzzer(True) else: self.box.set_buzzer(False) loop_count = loop_count + 1 sleep(0.1) if self.running and not self.card_present: logging.info("Grace period following card removal expired; shutting down equipment") logging.debug("wait_for_user_card_return() ends") def wait_for_timeout_grace_period_to_expire(self): """ Four posibilities: 1) user presses button with card in to renew session 2) user removes card and presses button to end session 3) user removes card but does not press button to end session 4) user forgot their card """ logging.info("Equipment usage timeout") self.box.has_button_been_pressed() # clear pending events logging.debug("Setting display to orange") self.box.set_display_color(ORANGE) # FIXME Does this really work? self.box.flash_display(ORANGE, 100, 1, ORANGE) logging.debug("Starting grace period") grace_seconds = 10 grace_start_time = time() loop_count = 0 while (self.running and (time() - grace_start_time) < grace_seconds): feed_watchdog("grace_timeout") #check for button press if self.box.has_button_been_pressed(): logging.info("Button was pressed, extending time out period") uid = self.box.read_RFID_card() if -1 < uid: # Card is still present session renewed logging.debug("Card still present, renew session") return else: # Card removed end session logging.debug("Card removed, end session") self.card_present = False return if (loop_count % 20) == 0: self.box.set_buzzer(True) else: self.box.set_buzzer(False) loop_count = loop_count + 1 sleep(0.1) # grace period expired # stop the buzzer logging.debug("Grace period expired") self.box.set_buzzer(False) # shutdown now, do not wait for email or card removal self.box.set_equipment_power_on(False) # was forgotten card? logging.debug("Checking for forgotten card") uid = self.box.read_RFID_card() if -1 < uid: # Card is still present logging.info("User card left in portal box. Sending user email.") logging.debug("Setting display to wipe blue") self.box.set_display_color_wipe(BLUE, 50) logging.debug("Getting user email ID from DB") user = self.db.get_user(self.authorized_uid) try: logging.debug("Mailing user") self.emailer.send(user[1], "Access Card left in PortalBox", "{} it appears you left your access card in a badge box for the {} in the {}".format(user[0], self.equipment_type, self.location)) except Exception as e: logging.error("{}".format(e)) logging.debug("Setting display to red") self.box.set_display_color(RED) while self.running and self.card_present: feed_watchdog("user_left_card") # wait for card to be removed uid = self.box.read_RFID_card() if 0 > uid: self.card_present = False logging.debug("Stopped running or card removed") else: # Card removed end session logging.debug("Card removed, session ends") self.card_present = False logging.debug("wait_for_timeout_grace_period_to_expire() ends") def exit(self): ''' Stop looping in all run states ''' logging.info("Service Exiting") feed_watchdog("service_exit") self.box.set_equipment_power_on(False) if self.running: if self.equipment_id: logging.debug("Logging exit-while-running to DB") self.db.log_shutdown_status(self.equipment_id, False) self.running = False else: # never made it to the run state logging.debug("Not running, just exit") sys.exit() def handle_interupt(self, signum, frame): ''' Stop the service from a signal''' logging.debug("Interrupted") feed_watchdog("service_interrupt") self.exit()
class PortalBoxApplication: ''' wrap code as a class to allow for clean sharing of objects between states ''' def __init__(self, settings): ''' Setup the bare minimun, defering as much as poosible to the run method so signal handlers can be configured in __main__ ''' self.exceeded_time = False self.running = False self.equipment_id = False self.box = PortalBox() self.settings = settings def __del__(self): ''' free resources after run ''' self.box.cleanup() def run(self): ''' Actually get ready to run... we defered initialization in order to configure signal handlers in __main__ but they should now be in place This corresponds to the transition from Start in FSM.odg see docs ''' # Step 1 Do a bit of a dance to show we are running self.box.set_display_color_wipe(RED, 10) # Set 2 Figure out our identity mac_address = format(get_mac_address(), 'x') logging.debug("Discovered Mac Address: %s", mac_address) # connect to backend database logging.debug("Connecting to database on host %s", self.settings['db']['host']) try: self.db = Database(self.settings['db']) logging.debug("Connected to Database") except Exception as e: logging.error("{}".format(e)) sys.exit(1) # be prepared to send emails try: self.emailer = Emailer(self.settings['email']) logging.debug("Cached email settings") except Exception as e: # should be unreachable logging.error("{}".format(e)) sys.exit(1) # give user hint we are makeing progress self.box.set_display_color_wipe(ORANGE, 10) # determine what we are profile = (-1, ) self.running = True while self.running and 0 > profile[0]: profile = self.db.get_equipment_profile(mac_address) if 0 > profile[0]: sleep(5) # only run if we have role, which we might not if systemd asked us to # shutdown before we discovered a role if 0 < profile[0]: # profile: # (int) equipment id # (int) equipment type id # (str) equipment type # (int) location id # (str) location # (int) time limit in minutes self.equipment_id = profile[0] self.equipment_type_id = profile[1] self.equipment_type = profile[2] self.location = profile[4] self.timeout_period = profile[5] logging.info( "Discovered identity. Type: %s(%s) Timeout period: %s", profile[2], self.equipment_type_id, self.timeout_period) self.db.log_started_status(self.equipment_id) self.box.set_display_color_wipe(GREEN, 10) self.timeout_period *= 60 # python threading wants seconds, DB has minutes self.proxy_uid = -1 self.training_mode = False self.wait_for_access_card() else: logging.info( "Running ending abnormally. Did not discover identity.") def wait_for_access_card(self): ''' Wait for a card and if we read a card decide what to do ''' self.box.set_display_color_wipe(BLUE, 15) # Run... loop endlessly waiting for RFID cards while self.running: # Scan for card uid = self.box.read_RFID_card() if -1 < uid: # we read a card... decide what to do card_type = self.db.get_card_type(uid) logging.debug("Card of type: %s was presented", card_type) if Database.SHUTDOWN_CARD == card_type: logging.info( "Shutdown Card: %s detected, triggering box shutdown", uid) self.db.log_shutdown_status(self.equipment_id, uid) self.box.set_display_color() os.system("shutdown -h now") elif Database.TRAINING_CARD == card_type: if self.db.is_training_card_for_equipment_type( uid, self.equipment_type_id): logging.info( "Trainer identitfied by card: %s is authorized to use equipment", uid) self.training_mode = True self.run_session(uid) self.training_mode = False else: self.wait_for_unauthorized_card_removal(uid) elif Database.USER_CARD == card_type: if self.db.is_user_authorized_for_equipment_type( uid, self.equipment_type_id): logging.info( "User identitfied by card: %s is authorized to use equipment", uid) self.run_session(uid) else: self.wait_for_unauthorized_card_removal(uid) else: self.wait_for_unauthorized_card_removal(uid) self.box.set_display_color_wipe(BLUE, 15) sleep(0.2) def run_session(self, user_id): ''' Allow user to use the equipment ''' logging.info( "Logging successful activation of equipment to backend database") self.db.log_access_attempt(user_id, self.equipment_id, True) self.authorized_uid = user_id self.box.set_display_color(GREEN) self.box.set_equipment_power_on(True) if 0 < self.timeout_period: self.exceeded_time = False self.activation_timeout = threading.Timer(self.timeout_period, self.timeout) self.activation_timeout.start() self.wait_for_authorized_card_removal() if not self.exceeded_time and 0 < self.timeout_period: self.activation_timeout.cancel() self.box.set_equipment_power_on(False) logging.info( "Logging completion of equipment access to backend database") self.db.log_access_completion(user_id, self.equipment_id) self.authorized_uid = -1 def timeout(self): ''' Called by timer thread when usage time is exceeeded ''' self.exceeded_time = True def wait_for_unauthorized_card_removal(self, uid): ''' Wait for card to be removed ''' logging.info("Card: %s is NOT authorized to use equipment", uid) self.db.log_access_attempt(uid, self.equipment_id, False) # We have to have a grace_counter because consecutive card reads currently fail grace_count = 0 #loop endlessly waiting for shutdown or card to be removed while self.running and grace_count < 2: # Scan for card uid = self.box.read_RFID_card() if -1 < uid: # we read a card grace_count = 0 else: # we did not read a card grace_count += 1 self.box.flash_display(RED, 100, 1, RED) def wait_for_authorized_card_removal(self): ''' Wait for card to be removed ''' self.card_present = True self.proxy_uid = -1 # We have to have a grace_counter because consecutive card reads currently fail grace_count = 0 #loop endlessly waiting for shutdown or card to be removed while self.running and self.card_present: # check for timeout if self.exceeded_time: self.wait_for_timeout_grace_period_to_expire() if self.card_present: # User pressed the button return to running self.exceeded_time = False if self.card_present: grace_count = 0 if -1 < self.proxy_uid: self.box.set_display_color(ORANGE) else: self.box.set_display_color(GREEN) self.activation_timeout = threading.Timer( self.timeout_period, self.timeout) self.activation_timeout.start() else: break # Scan for card uid = self.box.read_RFID_card() if -1 < uid and (uid == self.authorized_uid or uid == self.proxy_uid): # we read an authorized card grace_count = 0 else: # we did not read a card or we read the wrong card grace_count += 1 if grace_count > 2: self.wait_for_user_card_return() if self.card_present: grace_count = 0 if -1 < self.proxy_uid: self.box.set_display_color(ORANGE) else: self.box.set_display_color(GREEN) sleep(0.1) def wait_for_user_card_return(self): ''' Wait for a time for card to return before shutting down, button press shuts down immediately. We accomplish this using the card_present flag. By setting the flag to False immeditely we just return and the outer loop in wait_for_authorized_card_removal will also end. If we get the authorized card back we can toggle the flag back and return which will cause the outer loop to continue ''' self.card_present = False self.proxy_uid = -1 self.box.set_display_color(YELLOW) grace_count = 0 logging.info("Card Removed") self.box.has_button_been_pressed() # clear pending events while self.running and grace_count < 16: # Check for button press if self.box.has_button_been_pressed(): break # Scan for card uid = self.box.read_RFID_card() if -1 < uid: # we read a card if uid == self.authorized_uid: # card returned self.card_present = True break elif not self.training_mode: # trainers may not use proxy cards # check if proxy card if Database.PROXY_CARD == self.db.get_card_type(uid): self.card_present = True self.proxy_uid = uid break grace_count += 1 self.box.set_buzzer(True) self.box.flash_display(YELLOW, 100, 1, YELLOW) self.box.set_buzzer(False) if self.running and not self.card_present: logging.debug( "Grace period following card removal expired; shutting down equipment" ) def wait_for_timeout_grace_period_to_expire(self): """ Four posibilities: 1) user presses button with card in to renew session 2) user removes card and presses button to end session 3) user removes card but does not press button to end session 4) user forgot their card """ logging.info("Equipment usage timeout") grace_count = 0 self.box.has_button_been_pressed() # clear pending events self.box.set_display_color(ORANGE) while self.running and grace_count < 600: #check for button press if self.box.has_button_been_pressed(): logging.info("Button was pressed, extending time out period") uid = self.box.read_RFID_card() uid2 = self.box.read_RFID_card( ) #try twice since reader fails consecutive reads if -1 < uid or -1 < uid2: # Card is still present session renewed return else: # Card removed end session self.card_present = False return else: grace_count += 1 if 1 > (grace_count % 2): self.box.flash_display(ORANGE, 100, 1, ORANGE) if 1 > (grace_count % 20): self.box.set_buzzer(True) else: self.box.set_buzzer(False) sleep(0.1) # grace period expired # stop the buzzer self.box.set_buzzer(False) # shutdown now, do not wait for email or card removal self.box.set_equipment_power_on(False) # was forgotten card? uid = self.box.read_RFID_card() uid2 = self.box.read_RFID_card( ) #try twice since reader fails consecutive reads if -1 < uid or -1 < uid2: # Card is still present logging.info( "User card was left in portal box. Sending user an email.") self.box.set_display_color_wipe(BLUE, 50) user = self.db.get_user(self.authorized_uid) try: self.emailer.send( user[1], "Access Card left in PortalBox", "{} it appears you left your access card in a badge box for the {} in the {}" .format(user[0], self.equipment_type, self.location)) except Exception as e: logging.error("{}".format(e)) self.box.set_display_color(RED) while self.running and self.card_present: # wait for card to be removed... we need to make sure we don't have consecutive read failure uid = self.box.read_RFID_card() uid2 = self.box.read_RFID_card( ) #try twice since reader fails consecutive reads if 0 > uid and 0 > uid2: self.card_present = False else: # Card removed end session self.card_present = False def exit(self): ''' Stop looping in all run states ''' logging.info("Service Exiting") if self.running: if self.equipment_id: self.db.log_shutdown_status(self.equipment_id, False) self.running = False else: # never made it to the run state sys.exit() def handle_interupt(self, signum, frame): ''' Stop the service from a signal''' logging.debug("Interupted") self.exit()