diff --git a/requirements.txt b/requirements.txt index 0bdb51c..9ea319c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ ur-rtde~=1.4.1 redis~=3.5.3 -pyprocsync>=0.1.0 \ No newline at end of file +pyprocsync>=0.1.0 +requests +marshmallow~=3.11.1 \ No newline at end of file diff --git a/single_ursim_control/compiler.py b/single_ursim_control/compiler.py new file mode 100644 index 0000000..b3fff5e --- /dev/null +++ b/single_ursim_control/compiler.py @@ -0,0 +1,17 @@ +from plugins import AbstractCommand +from typing import List +from plugin_repository import PluginRepository +import logging + + +def compile_program(plugin_repository: PluginRepository, program_source: List[dict]) -> List[AbstractCommand]: + logger = logging.getLogger('compiler') + + compiled_program = [] + for command_source in program_source: + logger.debug(f"Compiling: [{command_source['name']}],{command_source['args']}") + compiled_program.append( + plugin_repository.get_compiler(command_source['name']).compile(**command_source['args']) + ) + + return compiled_program diff --git a/single_ursim_control/config.py b/single_ursim_control/config.py index f364352..2ee1650 100644 --- a/single_ursim_control/config.py +++ b/single_ursim_control/config.py @@ -1,3 +1,4 @@ +import sys import os @@ -7,4 +8,7 @@ class Config: SYNC_DELAY = float(os.environ.get("SYNC_DELAY", 1.0)) REDIS_URL = os.environ["REDIS_URL"] SYNC_TIMEOUT = os.environ.get("SYNC_TIMEOUT", None) # Wait infinity by default - ROBOT_ADDRESS = os.environ.get("ROBOT_ADDRESS") \ No newline at end of file + ROBOT_ADDRESS = os.environ.get("ROBOT_ADDRESS") + PROGRAM_URL = os.environ["PROGRAM_URL"] + DRY_RUN = ('--dry-run' in sys.argv) or bool(os.environ.get("DRY_RUN", False)) + DEBUG = ('--debug' in sys.argv) or bool(os.environ.get("DEBUG", False)) diff --git a/single_ursim_control/main.py b/single_ursim_control/main.py index dc9d43e..928e093 100644 --- a/single_ursim_control/main.py +++ b/single_ursim_control/main.py @@ -1,14 +1,16 @@ #!/usr/bin/env python3 -import os import sys from config import Config from plugins import SleepPlugin, SyncPlugin, WaitPlugin, URRTDEPlugin from plugin_repository import PluginRepository -from program_executor import ProgramExecutor +from program_executor import ProgramExecutor, ProgramExecutorStates from http_server import ControllerHTTPServer import logging import signal +from compiler import compile_program +from program_loader import load_program + class HTTPControl: @@ -22,43 +24,54 @@ class HTTPControl: return 201, "Will do sir!" -def main(): - # init +def main() -> int: + # init instance logging.basicConfig( stream=sys.stdout, format="%(asctime)s [%(levelname)s]: %(name)s: %(message)s", - level=logging.DEBUG + level=logging.DEBUG if Config.DEBUG else logging.INFO ) - http_server = ControllerHTTPServer(HTTPControl()) - http_server.start() - compiler_repo = PluginRepository() - compiler_repo.register_plugin(SleepPlugin) - compiler_repo.register_plugin(SyncPlugin) - compiler_repo.register_plugin(WaitPlugin) - compiler_repo.register_plugin(URRTDEPlugin) + logging.info("Registering available plugins...") + # Register all available plugins + plugin_repo = PluginRepository() + plugin_repo.register_plugin(SleepPlugin) + plugin_repo.register_plugin(SyncPlugin) + plugin_repo.register_plugin(WaitPlugin) + plugin_repo.register_plugin(URRTDEPlugin) - # Example code: - compiler_repo.load_plugin("sleep") - compiler_repo.load_plugin("sync") - compiler_repo.load_plugin("wait") - compiler_repo.load_plugin("ur_rtde") - program = [] - program.append(compiler_repo.get_compiler("sleep").compile(secs=2)) - program.append(compiler_repo.get_compiler("moveJ").compile([5.7386425805573555, -0.536165146212658, 1.6278685933351111, -2.661452576366153, -1.5683528658421044, 1.0096729722787197], 1.0, 4.0)) - program.append(compiler_repo.get_compiler("moveL").compile([-0.4, 0.1, -0.31, 3.142, 0, 0], 0.05, 0.75)) - program.append(compiler_repo.get_compiler("moveL").compile([-0.4, 0.1, -0.24, 3.142, 0, 0], 0.05, 0.75)) - program.append(compiler_repo.get_compiler("moveJ").compile([5.923472948343555, 0.032637657012293965, 0.2590068609959585, -0.2935643801854462, -2.7157323161031766, 4.71238898038469], 1.0, 4.0)) - program.append(compiler_repo.get_compiler("moveJ").compile([4.982042349817814, -0.5256931707006921, 1.620887276327134, -1.0993828958312282, -3.660653573132907, 5.271592472723674], 1.0, 4.0)) - program.append(compiler_repo.get_compiler("wait").compile()) - program.append(compiler_repo.get_compiler("sleep").compile(secs=3)) - program.append(compiler_repo.get_compiler("sync").compile(nodes=2, name="test")) - program.append(compiler_repo.get_compiler("sleep").compile(secs=10)) - program.append(compiler_repo.get_compiler("sleep").compile(secs=10)) + # Download the program + logging.info("Downloading program...") + try: + program_source = load_program(Config.PROGRAM_URL) + except Exception as e: + logging.error(f"Failed to download program: {e}! Exiting...") + logging.exception(e) + return 1 - # prepare execution - executor = ProgramExecutor(program, loop=True) + # Load required plugins + logging.info("Loading required plugins...") + try: + plugin_repo.load_plugins(program_source['load_plugins']) + except Exception as e: + logging.error(f"Error during plugin loading: {e}! Exiting...") + logging.exception(e) + return 2 + # Compile the program + logging.info("Compiling program...") + try: + program = compile_program(plugin_repo, program_source['program']) + except Exception as e: + logging.error(f"Error during compilation: {e}! Exiting...") + logging.exception(e) + return 3 + + # prepare the executor + logging.info("Preparing for execution...") + executor = ProgramExecutor(program, loop=False) + + # Setup signal handler def handle_stop_signal(signum, frame): logging.warning(f"Signal {signum} received. Aborting execution!") executor.abort() @@ -70,14 +83,28 @@ def main(): signal.signal(signal.SIGINT, handle_stop_signal) signal.signal(signal.SIGTERM, handle_stop_signal) - # start execution - executor.start() + # Actually execute + execution_success = True + if Config.DRY_RUN: + logging.info("DRY_RUN enabled. Dumping command descriptions and exiting!") + for i, command in enumerate(program): + logging.info(f"{i:04d}: {command.describe()}") + else: + logging.info("Starting execution...") + executor.start() + executor.join() - # End of execution - executor.join() - compiler_repo.close() - http_server.shutdown() + if executor.state == ProgramExecutorStates.DONE: + logging.info("Program executed successfully!") + else: + logging.error(f"Could not finish execution! Executor state: {executor.state.name}") + execution_success = False + + # Close all resources + logging.info("Cleaning up...") + plugin_repo.close() + return 0 if execution_success else 5 if __name__ == '__main__': - main() + sys.exit(main()) diff --git a/single_ursim_control/plugin_repository.py b/single_ursim_control/plugin_repository.py index 24f40f6..4ebbb27 100644 --- a/single_ursim_control/plugin_repository.py +++ b/single_ursim_control/plugin_repository.py @@ -3,7 +3,11 @@ from plugins import AbstractPlugin, AbstractCommandCompiler import logging -class ConflictingPlugins(BaseException): +class ConflictingPlugins(Exception): + pass + + +class UnknownPlugin(Exception): pass @@ -19,14 +23,25 @@ class PluginRepository: self._registered_plugins[plugin_class.plugin_name] = plugin_class self._logger.debug(f"Registered plugin: {plugin_class.plugin_name}") + def load_plugins(self, plugins: List[str]): + for plugin in plugins: + self.load_plugin(plugin) + def load_plugin(self, plugin: str): + self._logger.debug(f"Loading plugin: {plugin}") if plugin in self._loaded_plugins.keys(): self._logger.warning(f"Plugin {plugin} already loaded!") return - # create instance - plugin_instance = self._registered_plugins[plugin]() # config is statically loaded + # lookup plugin class + try: + plugin_cls = self._registered_plugins[plugin] + except KeyError: + raise UnknownPlugin(f"Tried to load unknown plugin: {plugin}") + + # Create instance + plugin_instance = plugin_cls() # config is statically loaded # load compilers compilers = plugin_instance.load_compilers() diff --git a/single_ursim_control/program_executor.py b/single_ursim_control/program_executor.py index e963eb5..d614aec 100644 --- a/single_ursim_control/program_executor.py +++ b/single_ursim_control/program_executor.py @@ -50,8 +50,14 @@ class ProgramExecutor(Thread): @property def state(self): + # Used to check the successfulness of the run as well return self._state + def stop_looping(self): + if self._loop: + self._logger.info("Looping disabled! Finishing current loop then exiting...") + self._loop = False + def run(self) -> None: self._state = ProgramExecutorStates.RUNNING self._logger.info("Start running program") @@ -61,7 +67,7 @@ class ProgramExecutor(Thread): for i, step in enumerate(self._program): self._pc = i self._current_step_desc = step.describe() - self._logger.debug(f"Executing step {self._pc}") + self._logger.debug(f"Executing step {self._pc:04d}") try: step.execute() diff --git a/single_ursim_control/program_loader.py b/single_ursim_control/program_loader.py new file mode 100644 index 0000000..9571f58 --- /dev/null +++ b/single_ursim_control/program_loader.py @@ -0,0 +1,14 @@ +from program_schema import ProgramSchema, SUPPORTED_PROGRAM_STRUCTURE_VERSION +import requests + + +def load_program(url: str) -> dict: + headers = { + 'Accept': 'application/json' + } + r = requests.get(url, headers=headers) + r.raise_for_status() + + program_schema = ProgramSchema(many=False) + + return program_schema.load(r.json()) # Might raise marshmallow exceptions diff --git a/single_ursim_control/program_schema.py b/single_ursim_control/program_schema.py new file mode 100644 index 0000000..cf3992a --- /dev/null +++ b/single_ursim_control/program_schema.py @@ -0,0 +1,23 @@ +from marshmallow import Schema, fields +from marshmallow.validate import Length, Equal +from marshmallow import RAISE + +SUPPORTED_PROGRAM_STRUCTURE_VERSION = 1 + + +class CommandSchema(Schema): + name = fields.Str(required=True) + args = fields.Dict(required=True) + + class Meta: + unknown = RAISE + + +class ProgramSchema(Schema): + version = fields.Int(required=True, validate=Equal(SUPPORTED_PROGRAM_STRUCTURE_VERSION)) + name = fields.Str() + load_plugins = fields.List(fields.Str(), required=True) + program = fields.Nested(CommandSchema, many=True, required=True, validate=Length(min=1)) + + class Meta: + unknown = RAISE