diff --git a/single_ursim_control/config.py b/single_ursim_control/config.py index e69de29..92f72ab 100644 --- a/single_ursim_control/config.py +++ b/single_ursim_control/config.py @@ -0,0 +1,6 @@ +import os + + +# Config is loaded statically at import time +class Config: + pass diff --git a/single_ursim_control/main.py b/single_ursim_control/main.py index 9a92cb5..e6ab3a4 100644 --- a/single_ursim_control/main.py +++ b/single_ursim_control/main.py @@ -1,5 +1,34 @@ +import os +import sys +from config import Config +from plugins import WaitPlugin +from plugin_repository import PluginRepository +from program_executor import ProgramExecutor +import logging + + def main(): - print("Hello world!") + # init + logging.basicConfig( + stream=sys.stdout, + format="%(asctime)s [%(levelname)s]: %(name)s: %(message)s", + level=logging.DEBUG + ) + compiler_repo = PluginRepository() + compiler_repo.register_plugin(WaitPlugin) + + # Example code: + compiler_repo.load_plugin("wait") + program = [] + program.append(compiler_repo.get_compiler("wait").compile(secs=2)) + + # execute: + executor = ProgramExecutor(program) + executor.start() + + # End of execution + executor.join() + compiler_repo.close() if __name__ == '__main__': diff --git a/single_ursim_control/plugin_repository.py b/single_ursim_control/plugin_repository.py new file mode 100644 index 0000000..b317a0b --- /dev/null +++ b/single_ursim_control/plugin_repository.py @@ -0,0 +1,35 @@ +from typing import Dict, List +from plugins import AbstractPlugin, AbstractCommandCompiler +import logging + + +class PluginRepository: + + def __init__(self): + self._registered_plugins: Dict[str, type(AbstractPlugin)] = {} + self._loaded_plugins: List[AbstractPlugin] = [] + self._command_compilers: Dict[str, AbstractCommandCompiler] = {} + self._logger = logging.getLogger("plugin_repository") + + def register_plugin(self, plugin_class: type(AbstractPlugin)): + self._registered_plugins[plugin_class.plugin_name] = plugin_class + self._logger.debug(f"Registered plugin: {plugin_class.plugin_name}") + + def load_plugin(self, plugin: str): + plugin_instance = self._registered_plugins[plugin]() # config is statically loaded + + self._loaded_plugins.append(plugin_instance) + + compilers = plugin_instance.load_compilers() + self._command_compilers.update(compilers) + self._logger.info(f"Loaded plugin: {plugin}") + self._logger.debug(f"Plugin {plugin} loaded the following commands: {', '.join(compilers.keys())}") + + def get_compiler(self, command: str) -> AbstractCommandCompiler: + return self._command_compilers[command] + + def close(self): + self._command_compilers = [] + for plugin in self._loaded_plugins: + plugin.close() + self._logger.info(f"Unloaded plugin: {plugin.plugin_name}") diff --git a/single_ursim_control/plugins/__init__.py b/single_ursim_control/plugins/__init__.py new file mode 100644 index 0000000..83ef11d --- /dev/null +++ b/single_ursim_control/plugins/__init__.py @@ -0,0 +1,2 @@ +from .abstract_plugin import AbstractCommand, AbstractCommandCompiler, AbstractPlugin +from .wait_plugin import WaitPlugin diff --git a/single_ursim_control/plugins/abstract_plugin.py b/single_ursim_control/plugins/abstract_plugin.py new file mode 100644 index 0000000..5397851 --- /dev/null +++ b/single_ursim_control/plugins/abstract_plugin.py @@ -0,0 +1,32 @@ +from typing import Dict +from abc import ABC, abstractmethod + + +class AbstractCommand(ABC): + + @abstractmethod + def execute(self): + pass + + @abstractmethod + def describe(self) -> dict: + pass + + +class AbstractCommandCompiler(ABC): + + @abstractmethod + def compile(self, *args, **kwargs) -> AbstractCommand: + pass + + +class AbstractPlugin(ABC): + plugin_name = "" + + @abstractmethod + def load_compilers(self) -> Dict[str, AbstractCommandCompiler]: + pass + + @abstractmethod + def close(self): + pass diff --git a/single_ursim_control/plugins/wait_plugin.py b/single_ursim_control/plugins/wait_plugin.py new file mode 100644 index 0000000..5884ea8 --- /dev/null +++ b/single_ursim_control/plugins/wait_plugin.py @@ -0,0 +1,53 @@ +from typing import Dict + +from .abstract_plugin import AbstractCommand, AbstractPlugin, AbstractCommandCompiler +import logging +import time + + +class WaitCommand(AbstractCommand): + + def __init__(self, logger: logging.Logger, secs: float): + + if type(secs) not in [float, int]: + raise ValueError("Secs must be float or int") + + if secs <= 0: + raise ValueError("Secs must be a positive integer") + + self._secs = secs + self._logger = logger + + def execute(self): + self._logger.debug(f"Sleeping for {self._secs} seconds") + time.sleep(self._secs) + self._logger.debug(f"Slept for {self._secs} seconds") + + def describe(self) -> dict: + return { + "secs": self._secs + } + + +class WaitCompiler(AbstractCommandCompiler): + + def __init__(self, logger: logging.Logger): + self._logger = logger + + def compile(self, secs: float) -> AbstractCommand: + return WaitCommand(self._logger, secs) + + +class WaitPlugin(AbstractPlugin): + plugin_name = "wait" + + def __init__(self): + self._logger = logging.getLogger("plugin").getChild("wait") + + def load_compilers(self) -> Dict[str, AbstractCommandCompiler]: + return { + "wait": WaitCompiler(self._logger) + } + + def close(self): + pass diff --git a/single_ursim_control/program_executor.py b/single_ursim_control/program_executor.py new file mode 100644 index 0000000..a9153ce --- /dev/null +++ b/single_ursim_control/program_executor.py @@ -0,0 +1,86 @@ +from typing import List +from threading import Thread +from plugins import AbstractCommand +from enum import Enum + +import logging + + +class ProgramExecutorStates(Enum): + PREPARING = 0 + RUNNING = 1 + DONE = 2 + ABORTED = 3 + CRASHED = 4 + + +class ProgramExecutor(Thread): + + def __init__( + self, + program: List[AbstractCommand], + loop: bool = False + ): + super().__init__() + self._program = program + self._loop = loop + self._current_step_desc = {} + self._pc = 0 + self._loop_counter = 0 + # TODO: jogging wait + + self._state = ProgramExecutorStates.PREPARING + + self._logger = logging.getLogger("program_executor") + + def abort(self): + self._state = ProgramExecutorStates.ABORTED + # TODO: implement abort + + def cont(self): + # TODO: jogging wait + pass + + def get_status(self) -> dict: + return { + "current_step": self._pc, + "program_length": len(self._program), + "step_description": self._current_step_desc, + "state": self._state.name, + "loop_count": self._loop_counter, + "config": { + "loop": self._loop + } + } + + @property + def state(self): + return self._state + + def run(self) -> None: + self._state = ProgramExecutorStates.RUNNING + self._logger.info("Start running program") + + while True: # needed for loop + self._loop_counter += 1 + for i, step in enumerate(self._program): + self._pc = i + self._current_step_desc = step.describe() + self._logger.debug(f"Executing step {self._pc}") + + try: + step.execute() + except Exception as e: + self._logger.exception(e) + self._state = ProgramExecutorStates.CRASHED + return + + # TODO: jogging wait + + if self._loop: + self._logger.debug("Looping program") + else: + self._logger.debug("Program ended") + break + + self._state = ProgramExecutorStates.DONE