diff --git a/setup.py b/setup.py index 2066166..d984817 100644 --- a/setup.py +++ b/setup.py @@ -9,10 +9,11 @@ setup( install_requires=[ 'toml', 'apscheduler', - 'paramiko' + 'paramiko', + 'click' ], entry_points={ - 'console_scripts': ['shepherd=shepherd.core:main'], + 'console_scripts': ['shepherd=shepherd.core:cli'], }, license='MIT license', description='Herd your mob of physically remote nodes', diff --git a/shepherd.toml b/shepherd.toml index 087c06b..ca263f0 100644 --- a/shepherd.toml +++ b/shepherd.toml @@ -1,8 +1,9 @@ [shepherd] plugin_path = "~/shepherd/" - plugins = ["picam"] + plugins = ["picam","test"] root_dir = "~/shepherd/" conf_edit_path = "~/shepherd.toml" + test =1 [picam] [[picam.trigger]] hour = "00-23" diff --git a/shepherd/config.py b/shepherd/config.py index 17cd6c1..32cfb8d 100644 --- a/shepherd/config.py +++ b/shepherd/config.py @@ -38,6 +38,7 @@ Root items that are not dicts are not supported, for instance both the following import re import toml from abc import ABC, abstractmethod +from copy import deepcopy from .freezedry import freezedryable, rehydrate @@ -58,6 +59,10 @@ class _ConfigDefinition(ABC): @abstractmethod def validate(self, value): + """ + Checks the supplied value to confirm that it complies with this ConfigDefinition. + Raises InvalidConfigError on failure. + """ pass @@ -121,7 +126,14 @@ class DictDef(_ConfigDefinition): self.def_dict[name] = newdef return newdef - def validate(self, value_dict): # pylint: disable=W0221 + def validate(self, value_dict): + """ + Checks the supplied value to confirm that it complies with this ConfigDefinition. + Raises InvalidConfigError on failure. + + This *can* modify the supplied value dict, inserting defaults for any child + ConfigDefinitions that are marked as optional. + """ def_set = set(self.def_dict.keys()) value_set = set(value_dict.keys()) @@ -210,6 +222,22 @@ class ConfigManager(): def __init__(self): self.root_config = {} self.confdefs = {} + self.frozen_config = {} + + @staticmethod + def _load_source(source): + """ + Accept a filepath or opened file representing a TOML file, or a direct dict, + and return a plain parsed dict. + """ + if isinstance(source, dict): # load from dict + return source + elif isinstance(source, str): # load from pathname + with open(source, 'r') as conf_file: + return toml.load(conf_file) + else: # load from file + return toml.load(source) + def load(self, source): """ @@ -219,13 +247,8 @@ class ConfigManager(): source: Either a dict config to load directly, a filepath to a TOML file, or an open file. """ - if isinstance(source, dict): # load from dict - self.root_config = source - elif isinstance(source, str): # load from pathname - with open(source, 'r') as conf_file: - self.root_config = toml.load(conf_file) - else: # load from file - self.root_config = toml.load(source) + self.root_config = self._load_source(source) + self._overlay(self.frozen_config, self.root_config) def load_overlay(self, source): """ @@ -237,17 +260,40 @@ class ConfigManager(): Args: source: Either the root dict of a data structure to load directly, a filepath to a TOML file, or an open TOML file. - """ + """ + self._overlay(self._load_source(source), self.root_config) + self._overlay(self.frozen_config, self.root_config) - if isinstance(source, dict): # load from dict - new_source = source - elif isinstance(source, str): # load from pathname - with open(source, 'r') as conf_file: - new_source = toml.load(conf_file) - else: # load from file - new_source = toml.load(source) + + def freeze_value(self, bundle_name, *field_names): + """ + Freeze the given config field so that subsequent calls to ``load`` and ``load_overlay`` + cannot change it. Can only be used for dict values or dict values nested in parent dicts. - self._overlay(new_source, self.root_config) + Args: + bundle_name: The name of the bundle to look for the field in. + *field_names: a series of strings that locate the config field, either a single + key or series of nested keys. + """ + + #Bundle names are really no different from any other nested dict + names = (bundle_name,) + field_names + + target_field = self.root_config + frozen_value = self.frozen_config + + # Cycle through nested names, creating frozen_config nested dicts as necessary + for name in names[:-1]: + target_field = target_field[name] + if name not in frozen_value: + frozen_value[name] = {} + frozen_value = frozen_value[name] + + + frozen_value[names[-1]] = target_field[names[-1]] + + + def add_confdef(self, bundle_name, confdef): """ @@ -290,10 +336,13 @@ class ConfigManager(): Get a config bundle called ``bundle_name`` and validate it against the corresponding config definition stored in the ConfigManager. If ``conf_def`` is supplied, it gets used instead. Returns a validated - config bundle dict. + config bundle dict. Note that as part of validation, optional keys that are missing will be - filled in with their default values (see ``DictDef``). + filled in with their default values (see ``DictDef``). This function will copy + the config bundle *after* validation, and so config loaded in the ConfManager will + be modified, but future ConfigManager manipulations won't change the returned config + bundle. Args: config_name: (str) Name of the config dict to find. @@ -308,14 +357,14 @@ class ConfigManager(): try: conf_def.validate(self.root_config[bundle_name]) except InvalidConfigError as e: - e.args = ("Module: " + bundle_name,) + e.args + e.args = ("Bundle: " + bundle_name,) + e.args raise - return self.root_config[bundle_name] + return deepcopy(self.root_config[bundle_name]) def get_config_bundles(self, bundle_names): """ Get multiple config bundles from the root dict at once, validating each one with the - corresponding confdef stored in the ConfigManager. + corresponding confdef stored in the ConfigManager. See ``get_config_bundle`` Args: bundle_names: A list of config bundle names to get. If dictionary is supplied, uses the values diff --git a/shepherd/control.py b/shepherd/control.py index 2324fca..89ef4c9 100644 --- a/shepherd/control.py +++ b/shepherd/control.py @@ -5,6 +5,7 @@ import requests import threading import json from urllib.parse import urlparse, urlunparse, urljoin +from collections import namedtuple import shepherd.plugin # Check for shepherd.new file in edit conf dir. If there, @@ -15,6 +16,35 @@ import shepherd.plugin #Start new thread, and push ID and core config to api.shepherd.distreon.net/client/update +class UpdateManager(): + def __init__(self): + pass + +class SequenceUpdate(): + Item = namedtuple('Item', ['sequence_number', 'content']) + def __init__(self): + self.items = [] + self._sequence_count = 0 + self._dirty = False + + def _next_sequence_number(self): + # TODO: need to establish a max sequence number, so that it can be compared to half + # that range and wrap around. + self._sequence_count +=1 + return self._sequence_count + + def mark_as_dirty(self): + self._dirty = True + + def add_item(self, item): + self.items.append(self.Item(self._next_sequence_number(), item)) + self.mark_as_dirty() + + def get_payload(): + pass + def process_ack(): + pass + client_id = None control_url = None api_key = None @@ -52,6 +82,10 @@ def init_control(core_config, plugin_config): global control_url global api_key + # On init, need to be able to quickly return the cached shepherd control layer if necessary. + + # Create the /update endpoint structure + root_dir = os.path.expanduser(core_config["root_dir"]) editconf_dir = os.path.dirname(os.path.expanduser(core_config["conf_edit_path"])) @@ -101,4 +135,5 @@ def _post_logs_job(): def post_logs(): post_logs_thread = threading.Thread(target=_post_logs_job, args=()) - post_logs_thread.start() \ No newline at end of file + post_logs_thread.start() + diff --git a/shepherd/core.py b/shepherd/core.py index 62e893d..916d8fb 100644 --- a/shepherd/core.py +++ b/shepherd/core.py @@ -1,19 +1,20 @@ -#!/usr/bin/env python3 +""" +Core shepherd module, tying together main service functionality. +""" -# depends on: -# python 3.4 (included in Raspbian Jessie) -# APScheduler - -import argparse import os +from pathlib import Path from datetime import datetime import toml +import logging +import click +from copy import deepcopy -import shepherd.scheduler -import shepherd.config -import shepherd.plugin -import shepherd.control +from . import scheduler +from . import config +from . import plugin +from . import control # Future implementations of checking config differences should be done on @@ -28,117 +29,191 @@ import shepherd.control # Fix this by saving the working config to /boot when new config applied # remotely. + +# Relative pathnames here are all relative to "root_dir" def define_core_config(confdef): - confdef.add_def("id", shepherd.config.StringDef()) - confdef.add_def("hostname", shepherd.config.StringDef(default="", optional=True)) - confdef.add_def("plugins", shepherd.config.StringArrayDef()) - confdef.add_def("plugin_dir", shepherd.config.StringDef()) - confdef.add_def("root_dir", shepherd.config.StringDef()) - confdef.add_def("conf_edit_path", shepherd.config.StringDef()) - - confdef.add_def("control_server", shepherd.config.StringDef()) - confdef.add_def("api_key", shepherd.config.StringDef()) - -def load_config(config_path,load_editconf): - # Load config from default location - confman = shepherd.config.ConfigManager() - confman.load(os.path.expanduser(config_path)) + """ + Defines the config definition by populating the ConfigDefinition passed in ``confdef`` - the same pattern plugins use + """ + confdef.add_def("name", config.StringDef( + helptext="Identifying name for this device")) + + confdef.add_def("hostname", + config.StringDef(default="", optional=True, + helptext="If set, changes the system hostname")) + confdef.add_def("plugin_dir", + config.StringDef(default="~/shepherd-plugins", optional=True, + helptext="Optional directory for Shepherd to look for plugins in.")) + confdef.add_def("root_dir", + config.StringDef(default="~/shepherd", optional=True, + helptext="Operating directory for shepherd to place working files.")) + confdef.add_def("custom_config_path", + config.StringDef(optional=True, + helptext="Path to custom config layer TOML file.")) + confdef.add_def("generated_config_path", + config.StringDef(default="shepherd-generated.toml", optional=True, + helptext="Path to custom file Shepherd will generate to show compiled config that was used and any errors in validation.")) + + confdef.add_def("control_server", config.StringDef()) + confdef.add_def("control_api_key", config.StringDef()) + + +def resolve_core_conf_paths(core_conf): + """ + Set the cwd to ``root_dir`` and resolve other core config paths relative to that. + Also expands out any "~" user characters. + """ + core_conf["root_dir"] = str(Path(core_conf["root_dir"]).expanduser().resolve()) + os.chdir(core_conf["root_dir"]) + core_conf["plugin_dir"] = str(Path(core_conf["plugin_dir"]).expanduser().resolve()) + core_conf["custom_config_path"] = str( + Path(core_conf["custom_config_path"]).expanduser().resolve()) + core_conf["generated_config_path"] = str( + Path(core_conf["generated_config_path"]).expanduser().resolve()) + + +def load_config_layer(confman, config_source): + """ + Load a config layer, find the necessary plugin classes, then validate it. + """ + # Load in config layer + confman.load_overlay(config_source) + + # Get the core config so we can find the plugin directory + core_config = confman.get_config_bundle("shepherd") + plugin_dir = core_config["plugin_dir"] + + # List other table names to get plugins we need to load + plugin_names = confman.get_bundle_names() + plugin_names.remove("shepherd") + + # Load plugins to get their conf defs + plugin_classes = plugin.find_plugins(plugin_names, plugin_dir) + for plugin_name, plugin_class in plugin_classes.items(): + new_conf_def = config.ConfDefinition() + plugin_class.define_config(new_conf_def) + confman.add_confdef(plugin_name, new_conf_def) + + # Get plugin configs + plugin_configs = confman.get_config_bundles(plugin_classes.keys()) + return (core_config, plugin_classes, plugin_configs) + + +def compile_config(default_config_path): + """ + Run through the process of assembling the various config layers, falling back to working + ones where necessary. Also gathers needed plugin classes in the process. + """ + # Create core confdef and populate it - core_confdef = shepherd.config.ConfDefinition() + core_confdef = config.ConfDefinition() define_core_config(core_confdef) - # attempt to retrive core config and validate it - core_conf = confman.get_config("shepherd", core_confdef) - edit_confman = None - conf_edit_message = None - - if load_editconf: - # Check for an edit_conf file, and try to load it and plugin configs - try: - edit_confman = shepherd.config.ConfigManager() - edit_confman.load(os.path.expanduser(core_conf["conf_edit_path"])) - core_edit_conf = edit_confman.get_config("shepherd", core_confdef) - - plugin_classes = shepherd.plugin.find_plugins( - core_edit_conf["plugins"]) - plugin_configs = edit_confman.get_plugin_configs(plugin_classes) - - except FileNotFoundError: - conf_edit_message = None - except shepherd.config.InvalidConfigError as e: - conf_edit_message = "Invalid config.\n " + str(e.args) - except toml.TomlDecodeError as e: - conf_edit_message = "TOML syntax error.\n" + str(e) - except Exception: - conf_edit_message = "Error processing new config" - else: - conf_edit_message = ("Successfully applied this config at:" + - str(datetime.now())) - confman = edit_confman - core_conf = core_edit_conf - - if conf_edit_message is not None: - shepherd.config.update_toml_message( - os.path.expanduser(core_conf["conf_edit_path"]), conf_edit_message) - - # if editconf failed, load current config for plugins - if confman is not edit_confman: - plugin_classes = shepherd.plugin.find_plugins(core_conf["plugins"]) - plugin_configs = confman.get_plugin_configs(plugin_classes) - - # If no editconf file was found, write out the current config as a template - if (conf_edit_message is None) and load_editconf: - confman.dump_to_file(os.path.expanduser(core_conf["conf_edit_path"]), - "Config generated at:" + str(datetime.now())) - - return (core_conf, plugin_classes, plugin_configs) - - - - - -def main(): - argparser = argparse.ArgumentParser(description="Keep track of a mob " - "of roaming Pis") - argparser.add_argument("configfile", nargs='?', metavar="configfile", - help="Path to configfile", default="shepherd.toml") - argparser.add_argument('-e', '--noedit',help="Disable the editable config temporarily", action="store_true", default=False) - argparser.add_argument("-t", "--test", help="Test and interface function of the from 'plugin:function'", - default=None) - - args = argparser.parse_args() - confman = shepherd.config.ConfigManager() - confman.load(os.path.expanduser(args.configfile)) + confman = config.ConfigManager() + confman.add_confdef("shepherd", core_confdef) - (core_conf, plugin_classes, plugin_configs) = load_config(args.configfile, not args.noedit) + # Default config. This must validate to continue. + try: + core_conf, plugin_classes, plugin_configs = load_config_layer( + confman, Path(default_config_path).expanduser()) + logging.info(F"Loaded default config from {default_config_path}") + except: + logging.error(F"Failed to load default config from {default_config_path}") + raise + + # Resolve and freeze local install paths that shouldn't be changed or affect loading custom config + confman.freeze_value("shepherd", "root_dir") + confman.freeze_value("shepherd", "plugin_dir") + confman.freeze_value("shepherd", "custom_config_path") + confman.freeze_value("shepherd", "generated_config_path") + resolve_core_conf_paths(core_conf) + + # Pull out custom config path and save current good root_config + custom_config_path = core_conf["custom_config_path"] + saved_root_config = deepcopy(confman.root_config) + + # Custom config layer. If this fails, maintain default config but continue on to Control layer + try: + core_conf, plugin_classes, plugin_configs = load_config_layer( + confman, custom_config_path) + logging.info(F"Loaded custom config from {custom_config_path}") + except Exception as e: + logging.error( + F"Failed to load custom config from {custom_config_path}. Falling back to default config.", exc_info=e) + confman.root_config = saved_root_config + + # Freeze Shepherd Control related config. + confman.freeze_value("shepherd", "control_server") + confman.freeze_value("shepherd", "control_api_key") + resolve_core_conf_paths(core_conf) + + # Save current good root_config + saved_root_config = deepcopy(confman.root_config) + + # Shepherd Control config layer. If this fails, maintain current local config. + try: + control_config = control.get_config(core_conf["root_dir"]) + try: + core_conf, plugin_classes, plugin_configs = load_config_layer( + confman, control_config) + logging.info(F"Loaded cached Shepherd Control config") + except Exception as e: + logging.error( + F"Failed to load cached Shepherd Control config. Falling back to local config.", exc_info=e) + confman.root_config = saved_root_config + except: + logging.warning("No cached Shepherd Control config available.") + + confman.dump_to_file(core_conf["generated_config_path"]) + + return core_conf, plugin_classes, plugin_configs + + +@click.group(invoke_without_command = True) +#help="Path to default config TOML file" + +@click.argument('default_config', default="shepherd-default.toml", type=click.Path()) +@click.pass_context +def cli(ctx, default_config): + """ + Core service. Expects the default config to be set as an argument. + """ + #argparser = argparse.ArgumentParser(description="Keep track of a mob " + # "of roaming Pis") + #argparser.add_argument("configfile", nargs='?', metavar="configfile", + # help="Path to configfile", default="shepherd.toml") + #argparser.add_argument( + # '-e', '--noedit', help="Disable the editable config temporarily", action="store_true", default=False) + #argparser.add_argument("-t", "--test", help="Test and interface function of the from 'plugin:function'", + # default=None) + + #args = argparser.parse_args() + + core_conf, plugin_classes, plugin_configs = compile_config(default_config) if args.test is None: - shepherd.control.init_control(core_conf, plugin_configs) + control.init_control(core_conf, plugin_configs) - shepherd.scheduler.init_scheduler(core_conf) + scheduler.init_scheduler(core_conf) + plugin.init_plugins(plugin_classes, plugin_configs, core_conf) + scheduler.restore_jobs() - shepherd.plugin.init_plugins(plugin_classes, plugin_configs, core_conf) - - if args.test is None: - shepherd.control.post_logs() - - shepherd.scheduler.restore_jobs() - print(str(datetime.now())) - if args.test is not None: - (test_plugin, test_func) = args.test.split(':') - func = getattr(shepherd.plugin.plugin_functions[test_plugin], test_func) - print(func()) + if ctx.invoked_subcommand is not None: return print('Press Ctrl+{0} to exit'.format('Break' if os.name == 'nt' else 'C')) - try: - shepherd.scheduler.start() + scheduler.start() except (KeyboardInterrupt, SystemExit): pass - -if __name__ == "__main__": - main() +@click.argument('plugin_function') +@cli.command() +def test(): + if args.test is not None: + (test_plugin, test_func) = args.test.split(':') + func = getattr(shepherd.plugin.plugin_functions[test_plugin], test_func) + print(func()) + return \ No newline at end of file diff --git a/shepherd/frame.jpg b/shepherd/frame.jpg deleted file mode 100644 index 9ad507f..0000000 Binary files a/shepherd/frame.jpg and /dev/null differ diff --git a/shepherd/plugin.py b/shepherd/plugin.py index 785ebd6..d1b064d 100644 --- a/shepherd/plugin.py +++ b/shepherd/plugin.py @@ -153,9 +153,11 @@ HookAttachment = namedtuple( class PluginInterface(): - # Class to handle the management of a single plugin. - # All interaction to or from the plugin to other Shepherd components or - # plugins should go through here. + ''' + Class to handle the management of a single plugin. + All interaction to or from the plugin to other Shepherd components or + plugins should go through here. + ''' def __init__(self, pluginname, pluginclass, pluginconfig, coreconfig): if not issubclass(pluginclass, Plugin): @@ -211,6 +213,19 @@ be called by other modules (which generally run in a seperate thread) def find_plugins(plugin_names, plugin_dir=None): + """ + Looks for the list of plugin names supplied and returns their classes. + Will first try for plugin modules and packages locally located in ``shepherd.plugins``, + then for modules and packages prefixed ``shepherd_`` located in the supplied ``plugin_dir`` + and lastly in the global import path. + + Args: + plugin_names: List of plugin names to try and load + plugin_dir: optional search path + + Returns: + Dict of plugin classes, with their names as keys + """ plugin_classes = {} for plugin_name in plugin_names: # First look for core plugins, then the plugin_dir, then in the general import path @@ -218,6 +233,7 @@ def find_plugins(plugin_names, plugin_dir=None): try: #mod = importlib.import_module("shepherd.plugins." + plugin_name) mod = importlib.import_module('.'+plugin_name, "shepherd.plugins") + #TODO - ModuleNotFoundError is also triggered here if the plugin has a dependancy that can't be found except ModuleNotFoundError: try: if (plugin_dir is not None) and (plugin_dir != ""): diff --git a/shepherd/plugins/flytrap.py b/shepherd/plugins/flytrap.py new file mode 100644 index 0000000..03ed104 --- /dev/null +++ b/shepherd/plugins/flytrap.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +import shepherd.config as shconf +import shepherd.plugin + +import sys +import os +import time +import argparse + + + +class FlytrapPlugin(shepherd.plugin.Plugin): + @staticmethod + def define_config(confdef): + confdef.add_def('servo_open_pulse', shconf.IntDef(default=1200, minval=800, maxval=2200)) + confdef.add_def('servo_closed_pulse', shconf.IntDef(default=1800, minval=800, maxval=2200)) + confdef.add_def('servo_open_time', shconf.IntDef(default=5)) + + def __init__(self, pluginInterface, config): + super().__init__(pluginInterface, config) + self.config = config + self.interface = pluginInterface + self.plugins = pluginInterface.other_plugins + self.hooks = pluginInterface.hooks + + self.root_dir = os.path.expanduser(pluginInterface.coreconfig["root_dir"]) + self.id = pluginInterface.coreconfig["id"] + + print("Flytrap config:") + print(self.config) + + self.interface.attach_hook("usbcam", "pre_cam", self.led_on) + self.interface.attach_hook("usbcam", "post_cam", self.uv_camera) + + self.interface.register_function(self.test) + + def uv_camera(self): + self.led_off() + self.led_uv_on() + self.plugins["usbcam"].run_cameras(" UV") + self.led_uv_off() + self.run_servo() + + def led_on(self): + self.plugins["scout"].set_out1(True) + + def led_off(self): + self.plugins["scout"].set_out1(False) + + def led_uv_on(self): + self.plugins["scout"].set_out2(True) + + def led_uv_off(self): + self.plugins["scout"].set_out2(False) + + def run_servo(self): + self.plugins["scout"].set_aux5v(True) + #self.door_servo_power.on() + time.sleep(0.5) + + self.plugins["scout"].set_pwm1(True, self.config["servo_open_pulse"]) + #self.door_servo.pulse_width = self.config["servo_open_pulse"] / 1000000 + time.sleep(self.config["servo_open_time"]) + + self.plugins["scout"].set_pwm1(True, self.config["servo_closed_pulse"]) + #self.door_servo.pulse_width = self.config["servo_closed_pulse"] / 1000000 + time.sleep(self.config["servo_open_time"]) + self.plugins["scout"].set_pwm1(False, self.config["servo_closed_pulse"]) + #self.door_servo.detach() + self.plugins["scout"].set_aux5v(False) + #self.door_servo_power.off() + + def test(self): + self.led_on() + time.sleep(1) + self.led_off() + self.run_servo() diff --git a/shepherd/plugins/mothtrap.py b/shepherd/plugins/mothtrap.py index 59695a0..dc4079d 100644 --- a/shepherd/plugins/mothtrap.py +++ b/shepherd/plugins/mothtrap.py @@ -8,17 +8,6 @@ import os import time import argparse -from gpiozero import OutputDevice, Device -from gpiozero.pins.pigpio import PiGPIOFactory - -from shepherd.plugins.betterservo import BetterServo - -Device.pin_factory = PiGPIOFactory() - - -MOTHTRAP_LED_PIN = 6 -MOTHTRAP_SERVO_PIN = 10 -MOTHTRAP_SERVO_POWER_PIN = 9 class MothtrapPlugin(shepherd.plugin.Plugin): @@ -41,50 +30,48 @@ class MothtrapPlugin(shepherd.plugin.Plugin): print("Mothtrap config:") print(self.config) - servo_max = self.config["servo_open_pulse"] / 1000000 - servo_min = self.config["servo_closed_pulse"] / 1000000 - - if servo_min > servo_max: - servo_min, servo_max = servo_max, servo_min + + + #servo_max = self.config["servo_open_pulse"] / 1000000 + #servo_min = self.config["servo_closed_pulse"] / 1000000 - self.door_servo = BetterServo(MOTHTRAP_SERVO_PIN, initial_value=None, - active_high=False, - min_pulse_width=servo_min-0.000001, - max_pulse_width=servo_max+0.000001) + #if servo_min > servo_max: + # servo_min, servo_max = servo_max, servo_min - print(F"Supplied min: {servo_min}, max: {servo_max}") - self.door_servo_power = OutputDevice(MOTHTRAP_SERVO_POWER_PIN, - active_high=True, - initial_value=False) + #print(F"Supplied min: {servo_min}, max: {servo_max}") - self.led_power = OutputDevice(MOTHTRAP_LED_PIN, - active_high=True, - initial_value=False) - self.interface.attach_hook("picam", "pre_cam", self.led_on) - self.interface.attach_hook("picam", "post_cam", self.led_off) - self.interface.attach_hook("picam", "post_cam", self.run_servo) + self.interface.attach_hook("usbcam", "pre_cam", self.led_on) + self.interface.attach_hook("usbcam", "post_cam", self.led_off) + self.interface.attach_hook("usbcam", "post_cam", self.run_servo) self.interface.register_function(self.test) def led_on(self): - self.led_power.on() + self.plugins["scout"].set_out1(True) + #self.led_power.on() def led_off(self): - self.led_power.off() + self.plugins["scout"].set_out1(False) + #self.led_power.off() def run_servo(self): - self.door_servo_power.on() + self.plugins["scout"].set_aux5v(True) + #self.door_servo_power.on() time.sleep(0.5) - self.door_servo.pulse_width = self.config["servo_open_pulse"] / 1000000 + self.plugins["scout"].set_pwm1(True, self.config["servo_open_pulse"]) + #self.door_servo.pulse_width = self.config["servo_open_pulse"] / 1000000 time.sleep(self.config["servo_open_time"]) - self.door_servo.pulse_width = self.config["servo_closed_pulse"] / 1000000 + self.plugins["scout"].set_pwm1(True, self.config["servo_closed_pulse"]) + #self.door_servo.pulse_width = self.config["servo_closed_pulse"] / 1000000 time.sleep(self.config["servo_open_time"]) - self.door_servo.detach() - self.door_servo_power.off() + self.plugins["scout"].set_pwm1(False, self.config["servo_closed_pulse"]) + #self.door_servo.detach() + self.plugins["scout"].set_aux5v(False) + #self.door_servo_power.off() def test(self): self.led_on() diff --git a/shepherd/plugins/usbcam.py b/shepherd/plugins/usbcam.py index 32ed615..3adfad1 100644 --- a/shepherd/plugins/usbcam.py +++ b/shepherd/plugins/usbcam.py @@ -142,6 +142,7 @@ class USBCamPlugin(shepherd.plugin.Plugin): self.interface.register_hook("pre_cam") self.interface.register_hook("post_cam") self.interface.register_function(self.camera_job) + self.interface.register_function(self.run_cameras) # do some camera init stuff print("USBCamera config:") @@ -292,16 +293,14 @@ class USBCamPlugin(shepherd.plugin.Plugin): print("Could not read camera "+camera_name + " on USB port "+device_path) - def camera_job(self): - self.hooks.pre_cam() - + def run_cameras(self, name_suffix = ""): connected_cams = OrderedDict(get_connected_cameras()) for defined_name, defined_usb_path in self.defined_cams.items(): if defined_usb_path in connected_cams: self._capture_image(connected_cams.pop( - defined_usb_path), defined_name) + defined_usb_path), defined_name+name_suffix) else: print("USB Camera "+defined_name+" on port " + @@ -310,12 +309,15 @@ class USBCamPlugin(shepherd.plugin.Plugin): for cam_name in self.wildcard_cams: if len(connected_cams) > 0: self._capture_image(connected_cams.popitem( - last=False)[1], cam_name) + last=False)[1], cam_name+name_suffix) else: print( "No connected USB cameras are currently left to match to "+cam_name+" ") break + def camera_job(self): + self.hooks.pre_cam() + self.run_cameras() self.hooks.post_cam() diff --git a/shepherd/test.jpg b/shepherd/test.jpg deleted file mode 100644 index 2255f5f..0000000 Binary files a/shepherd/test.jpg and /dev/null differ diff --git a/test.toml b/test.toml index 5c187ab..6c65bf8 100644 --- a/test.toml +++ b/test.toml @@ -1,28 +1,13 @@ [shepherd] - plugin_dir = "~/shepherd/" - plugins = ["scout"] + name = "test-node" + plugin_dir = "./" root_dir = "~/shepherd/" - conf_edit_path = "~/shepherd.toml" - id = "testnode" hostname = "shepherd-test" control_server = "api.shepherd.distreon.net" #control_server = "127.0.0.1:5000" - api_key = "v2EgvYzx79c8fCP4P7jlWxTZ3pc" + control_api_key = "v2EgvYzx79c8fCP4P7jlWxTZ3pc" [scout] boardver = "3" serialport = "/dev/ttyUSB0" -[usbcam] - [[usbcam.camera]] - name = "USB1" - usb_port = "*" - [[usbcam.camera]] - name = "USB2" - usb_port = "*" - [[usbcam.camera]] - name = "USB3" - usb_port = "3.1.2.1" - [[usbcam.trigger]] - hour = "*" - minute ="*/10" - second = "1" + diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..be4e3f7 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,18 @@ +import shepherd.config as config + +def test_freeze(): + confdef = config.ConfDefinition() + conf_def_dict = confdef.add_def('dictval', config.DictDef()) + conf_def_dict.add_def('intval', config.IntDef()) + conf_def_dict.add_def('strtval', config.StringDef()) + + confman = config.ConfigManager() + confman.add_confdef("test_bundle",confdef) + confman.load({"test_bundle": {'dictval': {'intval': 34, 'strval': "a"}}}) + + confman.freeze_value("test_bundle","dictval", "intval") + + confman.load({"test_bundle": {'dictval': {'intval': 34, 'strval': "b"}}}) + breakpoint() + print(confman.root_config) +