def __init__(self) -> None: """Initializes coordinator.""" # Initialize parent class super().__init__() # Initialize logger self.logger = Logger("Coordinator", "coordinator") self.logger.debug("Initializing coordinator") # Initialize state self.state = State() # Initialize environment state dict, TODO: remove this self.state.environment = { "sensor": {"desired": {}, "reported": {}}, "actuator": {"desired": {}, "reported": {}}, "reported_sensor_stats": { "individual": {"instantaneous": {}, "average": {}}, "group": {"instantaneous": {}, "average": {}}, }, } # Initialize recipe state dict, TODO: remove this self.state.recipe = { "recipe_uuid": None, "start_timestamp_minutes": None, "last_update_minute": None, } # Initialize managers self.recipe = RecipeManager(self.state) self.iot = IotManager(self.state, self.recipe) # type: ignore self.resource = ResourceManager(self.state, self.iot) # type: ignore self.network = NetworkManager(self.state) # type: ignore self.upgrade = UpgradeManager(self.state) # type: ignore # Initialize state machine transitions self.transitions = { modes.INIT: [modes.CONFIG, modes.ERROR, modes.SHUTDOWN], modes.CONFIG: [modes.SETUP, modes.ERROR, modes.SHUTDOWN], modes.SETUP: [modes.NORMAL, modes.ERROR, modes.SHUTDOWN], modes.NORMAL: [modes.LOAD, modes.ERROR, modes.SHUTDOWN], modes.LOAD: [modes.CONFIG, modes.ERROR, modes.SHUTDOWN], modes.RESET: [modes.INIT, modes.SHUTDOWN], modes.ERROR: [modes.RESET, modes.SHUTDOWN], } # Initialize state machine mode self.mode = modes.INIT
def __init__(self): """Initializes coordinator.""" self.logger.debug("Initializing coordinator") # Initialize mode and error self.mode = Modes.INIT # Initialize latest publish timestamp self.latest_publish_timestamp = 0 # Initialize state machine transitions self.transitions = Transitions(self, TRANSITION_TABLE) # Initialize coordinator event handler self.events = CoordinatorEvents(self) # Initialize managers self.iot = IoTManager(self.state, self.recipe) self.resource = ResourceManager(self.state, self, self.iot) self.connect = ConnectManager(self.state, self.iot) self.upgrade = UpgradeManager(self.state)
class CoordinatorManager(StateMachineManager): """Manages device state machine thread that spawns child threads to run recipes, read sensors, set actuators, manage control loops, sync data, and manage external events.""" # Initialize vars latest_publish_timestamp = 0.0 peripherals: Dict[str, StateMachineManager] = {} controllers: Dict[str, StateMachineManager] = {} new_config: bool = False def __init__(self) -> None: """Initializes coordinator.""" # Initialize parent class super().__init__() # Initialize logger self.logger = Logger("Coordinator", "coordinator") self.logger.debug("Initializing coordinator") # Initialize state self.state = State() # Initialize environment state dict, TODO: remove this self.state.environment = { "sensor": { "desired": {}, "reported": {} }, "actuator": { "desired": {}, "reported": {} }, "reported_sensor_stats": { "individual": { "instantaneous": {}, "average": {} }, "group": { "instantaneous": {}, "average": {} }, }, } # Initialize recipe state dict, TODO: remove this self.state.recipe = { "recipe_uuid": None, "start_timestamp_minutes": None, "last_update_minute": None, } # Initialize managers self.recipe = RecipeManager(self.state) self.iot = IotManager(self.state, self.recipe) # type: ignore self.recipe.set_iot(self.iot) self.resource = ResourceManager(self.state, self.iot) # type: ignore self.network = NetworkManager(self.state) # type: ignore self.upgrade = UpgradeManager(self.state) # type: ignore # Initialize state machine transitions self.transitions = { modes.INIT: [modes.CONFIG, modes.ERROR, modes.SHUTDOWN], modes.CONFIG: [modes.SETUP, modes.ERROR, modes.SHUTDOWN], modes.SETUP: [modes.NORMAL, modes.ERROR, modes.SHUTDOWN], modes.NORMAL: [modes.LOAD, modes.ERROR, modes.SHUTDOWN], modes.LOAD: [modes.CONFIG, modes.ERROR, modes.SHUTDOWN], modes.RESET: [modes.INIT, modes.SHUTDOWN], modes.ERROR: [modes.RESET, modes.SHUTDOWN], } # Initialize state machine mode self.mode = modes.INIT @property def mode(self) -> str: """Gets mode.""" return self._mode @mode.setter def mode(self, value: str) -> None: """Safely updates mode in state object.""" self._mode = value with self.state.lock: self.state.device["mode"] = value @property def config_uuid(self) -> Optional[str]: """ Gets config uuid from shared state. """ return self.state.device.get("config_uuid") # type: ignore @config_uuid.setter def config_uuid(self, value: Optional[str]) -> None: """ Safely updates config uuid in state. """ with self.state.lock: self.state.device["config_uuid"] = value @property def config_dict(self) -> Dict[str, Any]: """Gets config dict for config uuid in device config table.""" if self.config_uuid == None: return {} config = models.DeviceConfigModel.objects.get(uuid=self.config_uuid) return json.loads(config.json) # type: ignore @property def latest_environment_timestamp(self) -> float: """Gets latest environment timestamp from environment table.""" if not models.EnvironmentModel.objects.all(): return 0.0 else: environment = models.EnvironmentModel.objects.latest() return float(environment.timestamp.timestamp()) @property def manager_modes(self) -> Dict[str, str]: """Gets manager modes.""" self.logger.debug("Getting manager modes") # Get known manager modes modes = { "Coordinator": self.mode, "Recipe": self.recipe.mode, "Network": self.network.mode, "IoT": self.iot.mode, "Resource": self.resource.mode, } # Get peripheral manager modes for peripheral_name, peripheral_manager in self.peripherals.items(): modes[peripheral_name] = peripheral_manager.mode # Get controller manager modes for controller_name, controller_manager in self.controllers.items(): modes[controller_name] = controller_manager.mode # Return modes self.logger.debug("Returning modes: {}".format(modes)) return modes @property def manager_healths(self) -> Dict[str, str]: """Gets manager healths.""" self.logger.debug("Getting manager healths") # Initialize healths healths = {} # Get peripheral manager modes for peripheral_name, peripheral_manager in self.peripherals.items(): healths[ peripheral_name] = peripheral_manager.health # type: ignore # Return modes self.logger.debug("Returning healths: {}".format(healths)) return healths ##### STATE MACHINE FUNCTIONS ###################################################### def run(self) -> None: """Runs device state machine.""" # Loop forever while True: # Check if thread is shutdown if self.is_shutdown: break # Check for transitions if self.mode == modes.INIT: self.run_init_mode() elif self.mode == modes.CONFIG: self.run_config_mode() elif self.mode == modes.SETUP: self.run_setup_mode() elif self.mode == modes.NORMAL: self.run_normal_mode() elif self.mode == modes.LOAD: self.run_load_mode() elif self.mode == modes.ERROR: self.run_error_mode() elif self.mode == modes.RESET: self.run_reset_mode() elif self.mode == modes.SHUTDOWN: self.run_shutdown_mode() else: self.logger.critical("Invalid state machine mode") self.mode = modes.INVALID self.is_shutdown = True break def run_init_mode(self) -> None: """Runs init mode. Loads local data files and stored database state then transitions to config mode.""" self.logger.info("Entered INIT") # Load local data files and stored db state self.load_local_data_files() self.load_database_stored_state() # Transition to config mode on next state machine update self.mode = modes.CONFIG def run_config_mode(self) -> None: """Runs configuration mode. If device config is not set, loads 'unspecified' config then transitions to setup mode.""" self.logger.info("Entered CONFIG") # Check device config specifier file exists in repo try: with open(DEVICE_CONFIG_PATH) as f: config_name = f.readline().strip() except: env_dev_type = os.getenv("OPEN_AG_DEVICE_TYPE") if env_dev_type is None: config_name = "unspecified" message = "Unable to read {}, using unspecified config".format( DEVICE_CONFIG_PATH) else: config_name = env_dev_type message = "Unable to read {}, using {} config from env".format( DEVICE_CONFIG_PATH, config_name) self.logger.warning(message) # Create the directories if needed os.makedirs(os.path.dirname(DEVICE_CONFIG_PATH), exist_ok=True) # Write `unspecified` to device.txt with open(DEVICE_CONFIG_PATH, "w") as f: f.write("{}\n".format(config_name)) # Load device config self.logger.debug("Loading device config file: {}".format(config_name)) device_config = json.load( open("data/devices/{}.json".format(config_name))) # Check if config uuid changed, if so, adjust state if self.config_uuid != device_config["uuid"]: with self.state.lock: self.state.peripherals = {} self.state.controllers = {} set_nested_dict_safely( self.state.environment, ["reported_sensor_stats"], {}, self.state.lock, ) set_nested_dict_safely(self.state.environment, ["sensor", "reported"], {}, self.state.lock) self.config_uuid = device_config["uuid"] # Transition to setup mode on next state machine update self.mode = modes.SETUP def run_setup_mode(self) -> None: """Runs setup mode. Creates and spawns recipe, peripheral, and controller threads, waits for all threads to initialize then transitions to normal mode.""" self.logger.info("Entered SETUP") config_uuid = self.state.device["config_uuid"] # Spawn managers if not self.new_config: self.recipe.spawn() self.iot.spawn() self.resource.spawn() self.network.spawn() self.upgrade.spawn() # Create and spawn peripherals self.logger.debug("Creating and spawning peripherals") self.create_peripherals() self.spawn_peripherals() # Create and spawn controllers self.create_controllers() self.spawn_controllers() # Wait for all threads to initialize while not self.all_managers_initialized(): time.sleep(0.2) # Unset new config flag self.new_config = False # Transition to normal mode on next state machine update self.mode = modes.NORMAL def run_normal_mode(self) -> None: """Runs normal operation mode. Updates device state summary and stores device state in database, checks for new events and transitions.""" self.logger.info("Entered NORMAL") while True: # Overwrite system state in database every 100ms self.update_state() # Store environment state in every 10 minutes if time.time() - self.latest_environment_timestamp > 60 * 10: self.store_environment() # Check for events self.check_events() # Check for transitions if self.new_transition(modes.NORMAL): break # Update every 100ms time.sleep(0.1) def run_load_mode(self) -> None: """Runs load mode, shutsdown peripheral and controller threads then transitions to config mode.""" self.logger.info("Entered LOAD") # Shutdown peripherals and controllers self.shutdown_peripheral_threads() self.shutdown_controller_threads() # Initialize timeout parameters timeout = 10 start_time = time.time() # Loop forever while True: # Check if peripherals and controllers are shutdown if self.all_peripherals_shutdown( ) and self.all_controllers_shutdown(): self.logger.debug("All peripherals and controllers shutdown") break # Check for timeout if time.time() - start_time > timeout: self.logger.critical("Config threads did not shutdown") self.mode = modes.ERROR return # Update every 100ms time.sleep(0.1) # Set new config flag self.new_config = True # Transition to config mode on next state machine update self.mode = modes.CONFIG def run_reset_mode(self) -> None: """Runs reset mode. Shutsdown child threads then transitions to init.""" self.logger.info("Entered RESET") # Shutdown managers self.shutdown_peripheral_threads() self.shutdown_controller_threads() self.recipe.shutdown() self.iot.shutdown() # Transition to init mode on next state machine update self.mode = modes.INIT def run_error_mode(self) -> None: """Runs error mode. Shutsdown child threads, waits for new events and transitions.""" self.logger.info("Entered ERROR") # Shutsdown peripheral and controller threads self.shutdown_peripheral_threads() self.shutdown_controller_threads() # Loop forever while True: # Check for events self.check_events() # Check for transitions if self.new_transition(modes.ERROR): break # Update every 100ms time.sleep(0.1) ##### SUPPORT FUNCTIONS ############################################################ def update_state(self) -> None: """Updates stored state in database. If state does not exist, creates it.""" # TODO: Move this to state manager if not models.StateModel.objects.filter(pk=1).exists(): models.StateModel.objects.create( id=1, device=json.dumps(self.state.device), recipe=json.dumps(self.state.recipe), environment=json.dumps(self.state.environment), peripherals=json.dumps(self.state.peripherals), controllers=json.dumps(self.state.controllers), iot=json.dumps(self.state.iot), resource=json.dumps(self.state.resource), connect=json.dumps(self.state.network), upgrade=json.dumps(self.state.upgrade), ) else: models.StateModel.objects.filter(pk=1).update( device=json.dumps(self.state.device), recipe=json.dumps(self.state.recipe), environment=json.dumps(self.state.environment), peripherals=json.dumps(self.state.peripherals), controllers=json.dumps(self.state.controllers), iot=json.dumps(self.state.iot), resource=json.dumps(self.state.resource), connect=json.dumps(self.state.network), # TODO: migrate this upgrade=json.dumps(self.state.upgrade), ) def load_local_data_files(self) -> None: """ Loads local data files. """ self.logger.info("Loading local data files") # Load files with no verification dependencies first self.load_sensor_variables_file() self.load_actuator_variables_file() self.load_cultivars_file() self.load_cultivation_methods_file() # Load recipe files after sensor/actuator variables, cultivars, and # cultivation methods since verification depends on them self.load_recipe_files() # Load peripheral setup files after sensor/actuator variable since verification # depends on them self.load_peripheral_setup_files() # Load controller setup files after sensor/actuator variable since verification # depends on them self.load_controller_setup_files() # Load device config after peripheral setups since verification # depends on them self.load_device_config_files() def load_sensor_variables_file(self) -> None: """ Loads sensor variables file into database after removing all existing entries. """ self.logger.debug("Loading sensor variables file") # Load sensor variables and schema sensor_variables = json.load(open(SENSOR_VARIABLES_PATH)) sensor_variables_schema = json.load(open(SENSOR_VARIABLES_SCHEMA_PATH)) # Validate sensor variables with schema jsonschema.validate(sensor_variables, sensor_variables_schema) # Delete sensor variables tables models.SensorVariableModel.objects.all().delete() # Create sensor variables table for sensor_variable in sensor_variables: models.SensorVariableModel.objects.create( json=json.dumps(sensor_variable)) def load_actuator_variables_file(self) -> None: """ Loads actuator variables file into database after removing all existing entries. """ self.logger.debug("Loading actuator variables file") # Load actuator variables and schema actuator_variables = json.load(open(ACTUATOR_VARIABLES_PATH)) actuator_variables_schema = json.load( open(ACTUATOR_VARIABLES_SCHEMA_PATH)) # Validate actuator variables with schema jsonschema.validate(actuator_variables, actuator_variables_schema) # Delete actuator variables tables models.ActuatorVariableModel.objects.all().delete() # Create actuator variables table for actuator_variable in actuator_variables: models.ActuatorVariableModel.objects.create( json=json.dumps(actuator_variable)) def load_cultivars_file(self) -> None: """ Loads cultivars file into database after removing all existing entries.""" self.logger.debug("Loading cultivars file") # Load cultivars and schema cultivars = json.load(open(CULTIVARS_PATH)) cultivars_schema = json.load(open(CULTIVARS_SCHEMA_PATH)) # Validate cultivars with schema jsonschema.validate(cultivars, cultivars_schema) # Delete cultivars tables models.CultivarModel.objects.all().delete() # Create cultivars table for cultivar in cultivars: models.CultivarModel.objects.create(json=json.dumps(cultivar)) def load_cultivation_methods_file(self) -> None: """ Loads cultivation methods file into database after removing all existing entries. """ self.logger.debug("Loading cultivation methods file") # Load cultivation methods and schema cultivation_methods = json.load(open(CULTIVATION_METHODS_PATH)) cultivation_methods_schema = json.load( open(CULTIVATION_METHODS_SCHEMA_PATH)) # Validate cultivation methods with schema jsonschema.validate(cultivation_methods, cultivation_methods_schema) # Delete cultivation methods tables models.CultivationMethodModel.objects.all().delete() # Create cultivation methods table for cultivation_method in cultivation_methods: models.CultivationMethodModel.objects.create( json=json.dumps(cultivation_method)) def load_recipe_files(self) -> None: """Loads recipe files into database via recipe manager create or update function.""" self.logger.debug("Loading recipe files") # Get recipes for filepath in glob.glob(RECIPES_PATH): self.logger.debug("Loading recipe file: {}".format(filepath)) with open(filepath, "r") as f: json_ = f.read().replace("\n", "") message, code = self.recipe.create_or_update_recipe(json_) if code != 200: filename = filepath.split("/")[-1] error = "Unable to load {} -> {}".format(filename, message) self.logger.error(error) def load_peripheral_setup_files(self) -> None: """Loads peripheral setup files from codebase into database by creating new entries after deleting existing entries. Verification depends on sensor and actuator variables.""" self.logger.info("Loading peripheral setup files") # Get peripheral setups peripheral_setups = [] for filepath in glob.glob(PERIPHERAL_SETUP_FILES_PATH): self.logger.debug( "Loading peripheral setup file: {}".format(filepath)) peripheral_setups.append(json.load(open(filepath))) # Get get peripheral setup schema # TODO: Finish schema peripheral_setup_schema = json.load(open(PERIPHERAL_SETUP_SCHEMA_PATH)) # Validate peripheral setups with schema for peripheral_setup in peripheral_setups: jsonschema.validate(peripheral_setup, peripheral_setup_schema) # Delete all peripheral setup entries from database models.PeripheralSetupModel.objects.all().delete() # TODO: Validate peripheral setup variables with database variables # Create peripheral setup entries in database for peripheral_setup in peripheral_setups: models.PeripheralSetupModel.objects.create( json=json.dumps(peripheral_setup)) def load_controller_setup_files(self) -> None: """Loads controller setup files from codebase into database by creating new entries after deleting existing entries. Verification depends on sensor and actuator variables.""" self.logger.info("Loading controller setup files") # Get controller setups controller_setups = [] for filepath in glob.glob(CONTROLLER_SETUP_FILES_PATH): self.logger.debug( "Loading controller setup file: {}".format(filepath)) controller_setups.append(json.load(open(filepath))) # Get get controller setup schema controller_setup_schema = json.load(open(CONTROLLER_SETUP_SCHEMA_PATH)) # Validate peripheral setups with schema for controller_setup in controller_setups: jsonschema.validate(controller_setup, controller_setup_schema) # Delete all peripheral setup entries from database models.ControllerSetupModel.objects.all().delete() # TODO: Validate controller setup variables with database variables # Create peripheral setup entries in database for controller_setup in controller_setups: models.ControllerSetupModel.objects.create( json=json.dumps(controller_setup)) def load_device_config_files(self) -> None: """Loads device config files from codebase into database by creating new entries after deleting existing entries. Verification depends on peripheral setups. """ self.logger.info("Loading device config files") # Get devices device_configs = [] for filepath in glob.glob(DEVICE_CONFIG_FILES_PATH): self.logger.debug( "Loading device config file: {}".format(filepath)) device_configs.append(json.load(open(filepath))) # Get get device config schema # TODO: Finish schema (see optional objects) device_config_schema = json.load(open(DEVICE_CONFIG_SCHEMA_PATH)) # Validate device configs with schema for device_config in device_configs: jsonschema.validate(device_config, device_config_schema) # TODO: Validate device config with peripherals # TODO: Validate device config with varibles # Delete all device config entries from database models.DeviceConfigModel.objects.all().delete() # Create device config entry if new or update existing for device_config in device_configs: models.DeviceConfigModel.objects.create( json=json.dumps(device_config)) def load_database_stored_state(self) -> None: """ Loads stored state from database if it exists. """ self.logger.info("Loading database stored state") # Get stored state from database if not models.StateModel.objects.filter(pk=1).exists(): self.logger.info("No stored state in database") self.config_uuid = None return stored_state = models.StateModel.objects.filter(pk=1).first() # Load device state stored_device_state = json.loads(stored_state.device) # Load recipe state stored_recipe_state = json.loads(stored_state.recipe) self.recipe.recipe_uuid = stored_recipe_state["recipe_uuid"] self.recipe.recipe_name = stored_recipe_state["recipe_name"] self.recipe.duration_minutes = stored_recipe_state["duration_minutes"] self.recipe.start_timestamp_minutes = stored_recipe_state[ "start_timestamp_minutes"] self.recipe.last_update_minute = stored_recipe_state[ "last_update_minute"] self.recipe.stored_mode = stored_recipe_state["mode"] # Load peripherals state stored_peripherals_state = json.loads(stored_state.peripherals) for peripheral_name in stored_peripherals_state: self.state.peripherals[peripheral_name] = {} if "stored" in stored_peripherals_state[peripheral_name]: stored = stored_peripherals_state[peripheral_name]["stored"] self.state.peripherals[peripheral_name]["stored"] = stored # Load controllers state stored_controllers_state = json.loads(stored_state.controllers) for controller_name in stored_controllers_state: self.state.controllers[controller_name] = {} if "stored" in stored_controllers_state[controller_name]: stored = stored_controllers_state[controller_name]["stored"] self.state.controllers[controller_name]["stored"] = stored # Load iot state stored_iot_state = json.loads(stored_state.iot) self.state.iot["stored"] = stored_iot_state.get("stored", {}) def store_environment(self) -> None: """ Stores current environment state in environment table. """ models.EnvironmentModel.objects.create(state=self.state.environment) def create_peripherals(self) -> None: """ Creates peripheral managers. """ self.logger.info("Creating peripheral managers") # Verify peripherals are configured if self.config_dict.get("peripherals") == None: self.logger.info("No peripherals configured") return # Set var type mux_simulator: Optional[MuxSimulator] # Inintilize simulation parameters if os.environ.get("SIMULATE") == "true": simulate = True mux_simulator = MuxSimulator() else: simulate = False mux_simulator = None # Create thread locks i2c_lock = threading.RLock() # Create peripheral managers self.peripherals = {} peripheral_config_dicts = self.config_dict.get("peripherals", {}) for peripheral_config_dict in peripheral_config_dicts: self.logger.debug("Creating {}".format( peripheral_config_dict["name"])) # Get peripheral setup dict peripheral_uuid = peripheral_config_dict["uuid"] peripheral_setup_dict = self.get_peripheral_setup_dict( peripheral_uuid) self.logger.debug("UUID {}".format(peripheral_uuid)) # Verify valid peripheral config dict if peripheral_setup_dict == {}: self.logger.critical( "Invalid peripheral uuid in device " "config. Validator should have caught this.") continue # Get peripheral module and class name module_name = ("device.peripherals.modules." + peripheral_setup_dict["module_name"]) class_name = peripheral_setup_dict["class_name"] # Import peripheral library module_instance = __import__(module_name, fromlist=[class_name]) class_instance = getattr(module_instance, class_name) # Create peripheral manager peripheral_name = peripheral_config_dict["name"] peripheral = class_instance( name=peripheral_name, state=self.state, config=peripheral_config_dict, simulate=simulate, i2c_lock=i2c_lock, mux_simulator=mux_simulator, ) self.peripherals[peripheral_name] = peripheral def get_peripheral_setup_dict(self, uuid: str) -> Dict[str, Any]: """Gets peripheral setup dict for uuid in peripheral setup table.""" if not models.PeripheralSetupModel.objects.filter(uuid=uuid).exists(): return {} else: json_ = models.PeripheralSetupModel.objects.get(uuid=uuid).json peripheral_setup_dict = json.loads(json_) return peripheral_setup_dict # type: ignore def get_controller_setup_dict(self, uuid: str) -> Dict[str, Any]: """Gets controller setup dict for uuid in peripheral setup table.""" if not models.ControllerSetupModel.objects.filter(uuid=uuid).exists(): return {} else: json_ = models.ControllerSetupModel.objects.get(uuid=uuid).json controller_setup_dict = json.loads(json_) return controller_setup_dict # type: ignore def spawn_peripherals(self) -> None: """ Spawns peripherals. """ if self.peripherals == {}: self.logger.info("No peripheral threads to spawn") else: self.logger.info("Spawning peripherals") for name, manager in self.peripherals.items(): manager.spawn() def create_controllers(self) -> None: """ Creates controller managers. """ self.logger.info("Creating controller managers") # Verify controllers are configured if self.config_dict.get("controllers") == None: self.logger.info("No controllers configured") return # Create controller managers self.controllers = {} controller_config_dicts = self.config_dict.get("controllers", {}) for controller_config_dict in controller_config_dicts: self.logger.debug("Creating {}".format( controller_config_dict["name"])) # Get controller setup dict controller_uuid = controller_config_dict["uuid"] controller_setup_dict = self.get_controller_setup_dict( controller_uuid) # Verify valid controller config dict if controller_setup_dict == None: self.logger.critical( "Invalid controller uuid in device " "config. Validator should have caught this.") continue # Get controller module and class name module_name = ("device.controllers.modules." + controller_setup_dict["module_name"]) class_name = controller_setup_dict["class_name"] # Import controller library module_instance = __import__(module_name, fromlist=[class_name]) class_instance = getattr(module_instance, class_name) # Create controller manager controller_name = controller_config_dict["name"] controller_manager = class_instance(controller_name, self.state, controller_config_dict) self.controllers[controller_name] = controller_manager def spawn_controllers(self) -> None: """ Spawns controllers. """ if self.controllers == {}: self.logger.info("No controller threads to spawn") else: self.logger.info("Spawning controllers") for name, manager in self.controllers.items(): self.logger.debug("Spawning {}".format(name)) manager.spawn() def all_managers_initialized(self) -> bool: """Checks if all managers have initialized.""" if self.recipe.mode == modes.INIT: return False elif not self.all_peripherals_initialized(): return False elif not self.all_controllers_initialized(): return False return True def all_peripherals_initialized(self) -> bool: """Checks if all peripherals have initialized.""" for name, manager in self.peripherals.items(): if manager.mode == modes.INIT: return False return True def all_controllers_initialized(self) -> bool: """Checks if all controllers have initialized.""" for name, manager in self.controllers.items(): if manager.mode == modes.INIT: return False return True def shutdown_peripheral_threads(self) -> None: """Shutsdown all peripheral threads.""" for name, manager in self.peripherals.items(): manager.shutdown() def shutdown_controller_threads(self) -> None: """Shutsdown all controller threads.""" for name, manager in self.controllers.items(): manager.shutdown() def all_peripherals_shutdown(self) -> bool: """Check if all peripherals are shutdown.""" for name, manager in self.peripherals.items(): if manager.thread.is_alive(): return False return True def all_controllers_shutdown(self) -> bool: """Check if all controllers are shutdown.""" for name, manager in self.controllers.items(): if manager.thread.is_alive(): return False return True ##### EVENT FUNCTIONS ############################################################## def check_events(self) -> None: """Checks for a new event. Only processes one event per call, even if there are multiple in the queue. Events are processed first-in-first-out (FIFO).""" # Check for new events if self.event_queue.empty(): return # Get request request = self.event_queue.get() self.logger.debug("Received new request: {}".format(request)) # Get request parameters try: type_ = request["type"] except KeyError as e: message = "Invalid request parameters: {}".format(e) self.logger.exception(message) return # Execute request if type_ == events.RESET: self._reset() # Defined in parent class elif type_ == events.SHUTDOWN: self._shutdown() # Defined in parent class elif type_ == events.LOAD_DEVICE_CONFIG: self._load_device_config(request) else: self.logger.error( "Invalid event request type in queue: {}".format(type_)) def load_device_config(self, uuid: str) -> Tuple[str, int]: """Pre-processes load device config event request.""" self.logger.debug("Pre-processing load device config request") # Get filename of corresponding uuid filename = None for filepath in glob.glob(DEVICE_CONFIG_FILES_PATH): self.logger.debug(filepath) device_config = json.load(open(filepath)) if device_config["uuid"] == uuid: filename = filepath.split("/")[-1].replace(".json", "") # Verify valid config uuid if filename == None: message = "Invalid config uuid, corresponding filepath not found" self.logger.debug(message) return message, 400 # Check valid mode transition if enabled if not self.valid_transition(self.mode, modes.LOAD): message = "Unable to load device config from {} mode".format( self.mode) self.logger.debug(message) return message, 400 # Add load device config event request to event queue request = {"type": events.LOAD_DEVICE_CONFIG, "filename": filename} self.event_queue.put(request) # Successfully added load device config request to event queue message = "Loading config" return message, 200 def _load_device_config(self, request: Dict[str, Any]) -> None: """Processes load device config event request.""" self.logger.debug("Processing load device config request") # Get request parameters filename = request.get("filename") # Write config filename to device config path with open(DEVICE_CONFIG_PATH, "w") as f: f.write(str(filename) + "\n") # Transition to init mode on next state machine update self.mode = modes.LOAD
def test_init() -> None: state = State() manager = ResourceManager(state, IotManager(state, RecipeManager(state)))
class CoordinatorManager(Manager): """Manages device state machine thread that spawns child threads to run recipes, read sensors, set actuators, manage control loops, sync data, and manage external events.""" # Initialize logger extra = {"console_name": "Coordinator", "file_name": "Coordinator"} logger = logging.getLogger("coordinator") logger = logging.LoggerAdapter(logger, extra) # Initialize device mode and error _mode = None # Initialize state object, `state` serves as shared memory between threads state = State() # Initialize environment state dict state.environment = { "sensor": { "desired": {}, "reported": {} }, "actuator": { "desired": {}, "reported": {} }, "reported_sensor_stats": { "individual": { "instantaneous": {}, "average": {} }, "group": { "instantaneous": {}, "average": {} }, }, } # Initialize recipe state dict state.recipe = { "recipe_uuid": None, "start_timestamp_minutes": None, "last_update_minute": None, } # Initialize recipe object recipe = RecipeManager(state) # Initialize peripheral and controller managers peripherals = {} controllers = {} def __init__(self): """Initializes coordinator.""" self.logger.debug("Initializing coordinator") # Initialize mode and error self.mode = Modes.INIT # Initialize latest publish timestamp self.latest_publish_timestamp = 0 # Initialize state machine transitions self.transitions = Transitions(self, TRANSITION_TABLE) # Initialize coordinator event handler self.events = CoordinatorEvents(self) # Initialize managers self.iot = IoTManager(self.state, self.recipe) self.resource = ResourceManager(self.state, self, self.iot) self.connect = ConnectManager(self.state, self.iot) self.upgrade = UpgradeManager(self.state) @property def mode(self): """Gets mode.""" return self._mode @mode.setter def mode(self, value): """Safely updates mode in state object """ self._mode = value with self.state.lock: self.state.device["mode"] = value @property def commanded_mode(self): """ Gets commanded mode from shared state object. """ if "commanded_mode" in self.state.device: return self.state.device["commanded_mode"] else: return None @commanded_mode.setter def commanded_mode(self, value): """ Safely updates commanded mode in state object. """ with self.state.lock: self.state.device["commanded_mode"] = value @property def request(self): """ Gets request from shared state object. """ if "request" in self.state.device: return self.state.device["request"] else: return None @request.setter def request(self, value): """ Safely updates request in state object. """ with self.state.lock: self.state.device["request"] = value @property def response(self): """ Gets response from shared state object. """ if "response" in self.state.device: return self.state.device["response"] else: return None @response.setter def response(self, value): """ Safely updates response in state object. """ with self.state.lock: self.state.device["response"] = value @property def config_uuid(self): """ Gets config uuid from shared state. """ if "config_uuid" in self.state.device: return self.state.device["config_uuid"] else: return None @config_uuid.setter def config_uuid(self, value): """ Safely updates config uuid in state. """ with self.state.lock: self.state.device["config_uuid"] = value @property def commanded_config_uuid(self): """ Gets commanded config uuid from shared state. """ if "commanded_config_uuid" in self.state.device: return self.state.device["commanded_config_uuid"] else: return None @commanded_config_uuid.setter def commanded_config_uuid(self, value): """Safely updates commanded config uuid in state.""" with self.state.lock: self.state.device["commanded_config_uuid"] = value @property def config_dict(self): """Gets config dict for config uuid in device config table.""" if self.config_uuid == None: return None config = DeviceConfigModel.objects.get(uuid=self.config_uuid) return json.loads(config.json) @property def latest_environment_timestamp(self): """Gets latest environment timestamp from environment table.""" if not EnvironmentModel.objects.all(): return 0 else: environment = EnvironmentModel.objects.latest() return environment.timestamp.timestamp() def spawn(self, delay=None): """Spawns device thread.""" self.thread = threading.Thread(target=self.run_state_machine, args=(delay, )) self.thread.daemon = True self.thread.start() def run_state_machine(self, delay: float = 0) -> None: """Runs device state machine.""" # Wait for optional delay time.sleep(delay) self.logger.info("Spawning device thread") # Start state machine self.logger.info("Started state machine") while True: if self.mode == Modes.INIT: self.run_init_mode() elif self.mode == Modes.CONFIG: self.run_config_mode() elif self.mode == Modes.SETUP: self.run_setup_mode() elif self.mode == Modes.NORMAL: self.run_normal_mode() elif self.mode == Modes.LOAD: self.run_load_mode() elif self.mode == Modes.ERROR: self.run_error_mode() elif self.mode == Modes.RESET: self.run_reset_mode() def run_init_mode(self): """Runs initialization mode. Loads local data files and stored database state then transitions to config mode.""" self.logger.info("Entered INIT") # Load local data files self.load_local_data_files() # Load stored state from database self.load_database_stored_state() # Check for transitions if self.transitions.is_new(Modes.INIT): return # Transition to CONFIG self.mode = Modes.CONFIG def run_config_mode(self): """Runs configuration mode. If device config is not set, loads 'unspecified' config then transitions to setup mode.""" self.logger.info("Entered CONFIG") # Check device config specifier file exists in repo DEVICE_CONFIG_PATH = "data/config/device.txt" try: with open(DEVICE_CONFIG_PATH) as f: config_name = f.readline().strip() except: message = "Unable to read {}, using unspecified config".format( DEVICE_CONFIG_PATH) self.logger.warning(message) config_name = "unspecified" # Write `unspecified` to device.txt with open(DEVICE_CONFIG_PATH, "w") as f: f.write("{}\n".format(config_name)) # Load device config self.logger.debug("Loading device config file: {}".format(config_name)) device_config = json.load( open("data/devices/{}.json".format(config_name))) # Check if config uuid changed, if so, adjust state if self.config_uuid != device_config["uuid"]: with self.state.lock: self.state.peripherals = {} set_nested_dict_safely( self.state.environment, ["reported_sensor_stats"], {}, self.state.lock, ) set_nested_dict_safely(self.state.environment, ["sensor", "reported"], {}, self.state.lock) self.config_uuid = device_config["uuid"] # Check for transitions if self.transitions.is_new(Modes.CONFIG): return # Transition to SETUP self.mode = Modes.SETUP def run_setup_mode(self): """Runs setup mode. Creates and spawns recipe, peripheral, and controller threads, waits for all threads to initialize then transitions to normal mode.""" self.logger.info("Entered SETUP") config_uuid = self.state.device["config_uuid"] self.logger.debug("state.device.config_uuid = {}".format(config_uuid)) # Spawn the threads this object controls self.recipe.spawn() self.iot.spawn() self.resource.spawn() self.connect.spawn() self.upgrade.spawn() # Create peripheral managers and spawn threads self.create_peripherals() self.spawn_peripherals() # Create controller managers and spawn threads self.create_controllers() self.spawn_controllers() # Wait for all threads to initialize while not self.all_threads_initialized(): time.sleep(0.2) # Check for transitions if self.transitions.is_new(Modes.SETUP): return # Transition to NORMAL self.mode = Modes.NORMAL def run_normal_mode(self): """Runs normal operation mode. Updates device state summary and stores device state in database, checks for new events and transitions.""" self.logger.info("Entered NORMAL") while True: # Overwrite system state in database every 100ms self.update_state() # Store environment state in every 10 minutes if time.time() - self.latest_environment_timestamp > 60 * 10: self.store_environment() # Once a minute, publish any changed values if time.time() - self.latest_publish_timestamp > 60: self.latest_publish_timestamp = time.time() self.iot.publish() # Check for events self.events.check() # Check for transitions if self.transitions.is_new(Modes.NORMAL): break # Update every 100ms time.sleep(0.1) def run_load_mode(self): """Runs load mode, kills peripheral and controller threads then transitions to config mode.""" self.logger.info("Entered LOAD") # Kill peripheral and controller threads self.kill_peripheral_threads() self.kill_controller_threads() # Transition to CONFIG self.mode = Modes.CONFIG def run_reset_mode(self): """Runs reset mode. Kills child threads then transitions to init.""" self.logger.info("Entered RESET") # Kills child threads self.kill_peripheral_threads() self.kill_controller_threads() self.recipe.stop() self.iot.stop() # Transition to init on next state machine update self.mode = Modes.INIT def run_error_mode(self): """Runs error mode. Shutsdown child threads, waits for new events and transitions.""" self.logger.info("Entered ERROR") # Kills peripheral and controller threads self.kill_peripheral_threads() self.kill_controller_threads() # Loop forever while True: # Check for events self.events.check() # Check for transitions if self.transitions.is_new(Modes.ERROR): break # Update every 100ms time.sleep(0.1) def update_state(self): """Updates stored state in database. If state does not exist, creates it.""" # TODO: Move this to state manager if not StateModel.objects.filter(pk=1).exists(): StateModel.objects.create( id=1, device=json.dumps(self.state.device), recipe=json.dumps(self.state.recipe), environment=json.dumps(self.state.environment), peripherals=json.dumps(self.state.peripherals), controllers=json.dumps(self.state.controllers), iot=json.dumps(self.state.iot), resource=json.dumps(self.state.resource), connect=json.dumps(self.state.connect), upgrade=json.dumps(self.state.upgrade), ) else: StateModel.objects.filter(pk=1).update( device=json.dumps(self.state.device), recipe=json.dumps(self.state.recipe), environment=json.dumps(self.state.environment), peripherals=json.dumps(self.state.peripherals), controllers=json.dumps(self.state.controllers), iot=json.dumps(self.state.iot), resource=json.dumps(self.state.resource), connect=json.dumps(self.state.connect), upgrade=json.dumps(self.state.upgrade), ) def load_local_data_files(self): """ Loads local data files. """ self.logger.info("Loading local data files") # Load files with no verification dependencies first self.load_sensor_variables_file() self.load_actuator_variables_file() self.load_cultivars_file() self.load_cultivation_methods_file() # Load recipe files after sensor/actuator variables, cultivars, and # cultivation methods since verification depends on them self.load_recipe_files() # Load peripherals after sensor/actuator variable since verification # depends on them self.load_peripheral_setup_files() # Load device config after peripheral setups since verification # depends on them self.load_device_config_files() def load_sensor_variables_file(self): """ Loads sensor variables file into database after removing all existing entries. """ self.logger.debug("Loading sensor variables file") # Load sensor variables and schema sensor_variables = json.load( open("data/variables/sensor_variables.json")) sensor_variables_schema = json.load( open("data/schemas/sensor_variables.json")) # Validate sensor variables with schema validate(sensor_variables, sensor_variables_schema) # Delete sensor variables tables SensorVariableModel.objects.all().delete() # Create sensor variables table for sensor_variable in sensor_variables: SensorVariableModel.objects.create( json=json.dumps(sensor_variable)) def load_actuator_variables_file(self): """ Loads actuator variables file into database after removing all existing entries. """ self.logger.debug("Loading actuator variables file") # Load actuator variables and schema actuator_variables = json.load( open("data/variables/actuator_variables.json")) actuator_variables_schema = json.load( open("data/schemas/actuator_variables.json")) # Validate actuator variables with schema validate(actuator_variables, actuator_variables_schema) # Delete actuator variables tables ActuatorVariableModel.objects.all().delete() # Create actuator variables table for actuator_variable in actuator_variables: ActuatorVariableModel.objects.create( json=json.dumps(actuator_variable)) def load_cultivars_file(self): """ Loads cultivars file into database after removing all existing entries.""" self.logger.debug("Loading cultivars file") # Load cultivars and schema cultivars = json.load(open("data/cultivations/cultivars.json")) cultivars_schema = json.load(open("data/schemas/cultivars.json")) # Validate cultivars with schema validate(cultivars, cultivars_schema) # Delete cultivars tables CultivarModel.objects.all().delete() # Create cultivars table for cultivar in cultivars: CultivarModel.objects.create(json=json.dumps(cultivar)) def load_cultivation_methods_file(self): """ Loads cultivation methods file into database after removing all existing entries. """ self.logger.debug("Loading cultivation methods file") # Load cultivation methods and schema cultivation_methods = json.load( open("data/cultivations/cultivation_methods.json")) cultivation_methods_schema = json.load( open("data/schemas/cultivation_methods.json")) # Validate cultivation methods with schema validate(cultivation_methods, cultivation_methods_schema) # Delete cultivation methods tables CultivationMethodModel.objects.all().delete() # Create cultivation methods table for cultivation_method in cultivation_methods: CultivationMethodModel.objects.create( json=json.dumps(cultivation_method)) def load_recipe_files(self): """Loads recipe files into database via recipe manager create or update function.""" self.logger.debug("Loading recipe files") # Get recipes for filepath in glob.glob("data/recipes/*.json"): self.logger.debug("Loading recipe file: {}".format(filepath)) with open(filepath, "r") as f: json_ = f.read().replace("\n", "") message, code = self.recipe.events.create_or_update_recipe( json_) if code != 200: filename = filepath.split("/")[-1] error = "Unable to load {} -> {}".format(filename, message) self.logger.error(error) def load_peripheral_setup_files(self): """ Loads peripheral setup files from codebase into database by creating new entries after deleting existing entries. Verification depends on sensor/actuator variables. """ self.logger.info("Loading peripheral setup files") # Get peripheral setups peripheral_setups = [] for filepath in glob.glob( "device/peripherals/modules/*/setups/*.json"): self.logger.debug( "Loading peripheral setup file: {}".format(filepath)) peripheral_setups.append(json.load(open(filepath))) # Get get peripheral setup schema # TODO: Finish schema peripheral_setup_schema = json.load( open("data/schemas/peripheral_setup.json")) # Validate peripheral setups with schema for peripheral_setup in peripheral_setups: validate(peripheral_setup, peripheral_setup_schema) # Delete all peripheral setup entries from database PeripheralSetupModel.objects.all().delete() # TODO: Validate peripheral setup variables with database variables # Create peripheral setup entries in database for peripheral_setup in peripheral_setups: PeripheralSetupModel.objects.create( json=json.dumps(peripheral_setup)) def load_device_config_files(self): """ Loads device config files from codebase into database by creating new entries after deleting existing entries. Verification depends on peripheral setups. """ self.logger.info("Loading device config files") # Get devices device_configs = [] for filepath in glob.glob("data/devices/*.json"): self.logger.debug( "Loading device config file: {}".format(filepath)) device_configs.append(json.load(open(filepath))) # Get get device config schema # TODO: Finish schema (see optional objects) device_config_schema = json.load( open("data/schemas/device_config.json")) # Validate device configs with schema for device_config in device_configs: validate(device_config, device_config_schema) # TODO: Validate device config with peripherals # TODO: Validate device config with varibles # Delete all device config entries from database DeviceConfigModel.objects.all().delete() # Create device config entry if new or update existing for device_config in device_configs: DeviceConfigModel.objects.create(json=json.dumps(device_config)) def load_database_stored_state(self): """ Loads stored state from database if it exists. """ self.logger.info("Loading database stored state") # Get stored state from database if not StateModel.objects.filter(pk=1).exists(): self.logger.info("No stored state in database") self.config_uuid = None return stored_state = StateModel.objects.filter(pk=1).first() # Load device state stored_device_state = json.loads(stored_state.device) # Load recipe state stored_recipe_state = json.loads(stored_state.recipe) self.recipe.recipe_uuid = stored_recipe_state["recipe_uuid"] self.recipe.recipe_name = stored_recipe_state["recipe_name"] self.recipe.duration_minutes = stored_recipe_state["duration_minutes"] self.recipe.start_timestamp_minutes = stored_recipe_state[ "start_timestamp_minutes"] self.recipe.last_update_minute = stored_recipe_state[ "last_update_minute"] self.recipe.stored_mode = stored_recipe_state["mode"] # Load peripherals state stored_peripherals_state = json.loads(stored_state.peripherals) for peripheral_name in stored_peripherals_state: self.state.peripherals[peripheral_name] = {} if "stored" in stored_peripherals_state[peripheral_name]: stored = stored_peripherals_state[peripheral_name]["stored"] self.state.peripherals[peripheral_name]["stored"] = stored # Load controllers state stored_controllers_state = json.loads(stored_state.controllers) for controller_name in stored_controllers_state: self.state.controllers[controller_name] = {} if "stored" in stored_controllers_state[controller_name]: stored = stored_controllers_state[controller_name]["stored"] self.state.controllers[controller_name]["stored"] = stored def store_environment(self): """ Stores current environment state in environment table. """ EnvironmentModel.objects.create(state=self.state.environment) def create_peripherals(self): """ Creates peripheral managers. """ self.logger.info("Creating peripheral managers") # Verify peripherals are configured if self.config_dict["peripherals"] == None: self.logger.info("No peripherals configured") return # Inintilize simulation parameters if os.environ.get("SIMULATE") == "true": simulate = True mux_simulator = MuxSimulator() else: simulate = False mux_simulator = None # Create thread locks i2c_lock = threading.RLock() # Create peripheral managers self.peripherals = {} for peripheral_config_dict in self.config_dict["peripherals"]: self.logger.debug("Creating {}".format( peripheral_config_dict["name"])) # Get peripheral setup dict peripheral_uuid = peripheral_config_dict["uuid"] peripheral_setup_dict = self.get_peripheral_setup_dict( peripheral_uuid) # Verify valid peripheral config dict if peripheral_setup_dict == None: self.logger.critical( "Invalid peripheral uuid in device " "config. Validator should have caught this.") continue # Get peripheral module and class name module_name = ("device.peripherals.modules." + peripheral_setup_dict["module_name"]) class_name = peripheral_setup_dict["class_name"] # Import peripheral library module_instance = __import__(module_name, fromlist=[class_name]) class_instance = getattr(module_instance, class_name) # Create peripheral manager peripheral_name = peripheral_config_dict["name"] peripheral = class_instance( name=peripheral_name, state=self.state, config=peripheral_config_dict, simulate=simulate, i2c_lock=i2c_lock, mux_simulator=mux_simulator, ) self.peripherals[peripheral_name] = peripheral def get_peripheral_setup_dict(self, uuid): """ Gets peripheral setup dict for uuid in peripheral setup table. """ if not PeripheralSetupModel.objects.filter(uuid=uuid).exists(): return None return json.loads(PeripheralSetupModel.objects.get(uuid=uuid).json) def spawn_peripherals(self): """ Spawns peripheral threads. """ if self.peripherals == {}: self.logger.info("No peripheral threads to spawn") else: self.logger.info("Spawning peripheral threads") for peripheral_name in self.peripherals: self.peripherals[peripheral_name].spawn() def create_controllers(self): """ Creates controller managers. """ self.logger.info("Creating controller managers") # Verify controllers are configured if self.config_dict["controllers"] == None: self.logger.info("No controllers configured") return # Create controller managers self.controllers = {} for controller_config_dict in self.config_dict["controllers"]: self.logger.debug("Creating {}".format( controller_config_dict["name"])) # Get controller setup dict controller_uuid = controller_config_dict["uuid"] controller_setup_dict = self.get_controller_setup_dict( controller_uuid) # Verify valid controller config dict if controller_setup_dict == None: self.logger.critical( "Invalid controller uuid in device " "config. Validator should have caught this.") continue # Get controller module and class name module_name = ("device.controllers.drivers." + controller_setup_dict["module_name"]) class_name = controller_setup_dict["class_name"] # Import controller library module_instance = __import__(module_name, fromlist=[class_name]) class_instance = getattr(module_instance, class_name) # Create controller manager controller_name = controller_config_dict["name"] controller_manager = class_instance(controller_name, self.state, controller_config_dict) self.controllers[controller_name] = controller_manager def spawn_controllers(self): """ Spawns controller threads. """ if self.controllers == {}: self.logger.info("No controller threads to spawn") else: self.logger.info("Spawning controller threads") for controller_name in self.controllers: self.controllers[controller_name].spawn() def all_threads_initialized(self): """ Checks that all recipe, peripheral, and controller theads are initialized. """ if self.state.recipe["mode"] == Modes.INIT: return False elif not self.all_peripherals_initialized(): return False elif not self.all_controllers_initialized(): return False return True def all_peripherals_initialized(self): """ Checks that all peripheral threads have transitioned from INIT. """ for peripheral_name in self.state.peripherals: peripheral_state = self.state.peripherals[peripheral_name] # Check if mode in peripheral state if "mode" not in peripheral_state: return False # Check if mode either init or setup if peripheral_state["mode"] == Modes.INIT: return False return True def all_controllers_initialized(self): """ Checks that all controller threads have transitioned from INIT. """ for controller_name in self.state.controllers: controller_state = self.state.controllers[controller_name] if controller_state["mode"] == Modes.INIT: return False return True def kill_peripheral_threads(self): """Kills all peripheral threads.""" for peripheral_name, peripheral_manager in self.peripherals.items(): peripheral_manager.thread_is_active = False def kill_controller_threads(self): """Kills all controller threads.""" for controller_name, controller_manager in self.controllers.items(): controlled_manager.thread_is_active = False