Docs, added layer system to config, updated plugins

fix-v0.2
Tom Wilson 6 years ago
parent 8a32a252d4
commit 9c1dda6372

@ -9,10 +9,11 @@ setup(
install_requires=[ install_requires=[
'toml', 'toml',
'apscheduler', 'apscheduler',
'paramiko' 'paramiko',
'click'
], ],
entry_points={ entry_points={
'console_scripts': ['shepherd=shepherd.core:main'], 'console_scripts': ['shepherd=shepherd.core:cli'],
}, },
license='MIT license', license='MIT license',
description='Herd your mob of physically remote nodes', description='Herd your mob of physically remote nodes',

@ -1,8 +1,9 @@
[shepherd] [shepherd]
plugin_path = "~/shepherd/" plugin_path = "~/shepherd/"
plugins = ["picam"] plugins = ["picam","test"]
root_dir = "~/shepherd/" root_dir = "~/shepherd/"
conf_edit_path = "~/shepherd.toml" conf_edit_path = "~/shepherd.toml"
test =1
[picam] [picam]
[[picam.trigger]] [[picam.trigger]]
hour = "00-23" hour = "00-23"

@ -38,6 +38,7 @@ Root items that are not dicts are not supported, for instance both the following
import re import re
import toml import toml
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from copy import deepcopy
from .freezedry import freezedryable, rehydrate from .freezedry import freezedryable, rehydrate
@ -58,6 +59,10 @@ class _ConfigDefinition(ABC):
@abstractmethod @abstractmethod
def validate(self, value): def validate(self, value):
"""
Checks the supplied value to confirm that it complies with this ConfigDefinition.
Raises InvalidConfigError on failure.
"""
pass pass
@ -121,7 +126,14 @@ class DictDef(_ConfigDefinition):
self.def_dict[name] = newdef self.def_dict[name] = newdef
return 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()) def_set = set(self.def_dict.keys())
value_set = set(value_dict.keys()) value_set = set(value_dict.keys())
@ -210,6 +222,22 @@ class ConfigManager():
def __init__(self): def __init__(self):
self.root_config = {} self.root_config = {}
self.confdefs = {} 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): def load(self, source):
""" """
@ -219,13 +247,8 @@ class ConfigManager():
source: Either a dict config to load directly, a filepath to a TOML file, source: Either a dict config to load directly, a filepath to a TOML file,
or an open file. or an open file.
""" """
if isinstance(source, dict): # load from dict self.root_config = self._load_source(source)
self.root_config = source self._overlay(self.frozen_config, self.root_config)
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)
def load_overlay(self, source): def load_overlay(self, source):
""" """
@ -238,16 +261,39 @@ class ConfigManager():
source: Either the root dict of a data structure to load directly, a filepath to a TOML file, source: Either the root dict of a data structure to load directly, a filepath to a TOML file,
or an open TOML file. or an open TOML file.
""" """
self._overlay(self._load_source(source), self.root_config)
self._overlay(self.frozen_config, self.root_config)
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.
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]]
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)
self._overlay(new_source, self.root_config)
def add_confdef(self, bundle_name, confdef): def add_confdef(self, bundle_name, confdef):
""" """
@ -293,7 +339,10 @@ class ConfigManager():
config bundle dict. config bundle dict.
Note that as part of validation, optional keys that are missing will be 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: Args:
config_name: (str) Name of the config dict to find. config_name: (str) Name of the config dict to find.
@ -308,14 +357,14 @@ class ConfigManager():
try: try:
conf_def.validate(self.root_config[bundle_name]) conf_def.validate(self.root_config[bundle_name])
except InvalidConfigError as e: except InvalidConfigError as e:
e.args = ("Module: " + bundle_name,) + e.args e.args = ("Bundle: " + bundle_name,) + e.args
raise raise
return self.root_config[bundle_name] return deepcopy(self.root_config[bundle_name])
def get_config_bundles(self, bundle_names): def get_config_bundles(self, bundle_names):
""" """
Get multiple config bundles from the root dict at once, validating each one with the 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: Args:
bundle_names: A list of config bundle names to get. If dictionary is supplied, uses the values bundle_names: A list of config bundle names to get. If dictionary is supplied, uses the values

@ -5,6 +5,7 @@ import requests
import threading import threading
import json import json
from urllib.parse import urlparse, urlunparse, urljoin from urllib.parse import urlparse, urlunparse, urljoin
from collections import namedtuple
import shepherd.plugin import shepherd.plugin
# Check for shepherd.new file in edit conf dir. If there, # 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 #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 client_id = None
control_url = None control_url = None
api_key = None api_key = None
@ -52,6 +82,10 @@ def init_control(core_config, plugin_config):
global control_url global control_url
global api_key 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"]) root_dir = os.path.expanduser(core_config["root_dir"])
editconf_dir = os.path.dirname(os.path.expanduser(core_config["conf_edit_path"])) editconf_dir = os.path.dirname(os.path.expanduser(core_config["conf_edit_path"]))
@ -102,3 +136,4 @@ def _post_logs_job():
def post_logs(): def post_logs():
post_logs_thread = threading.Thread(target=_post_logs_job, args=()) post_logs_thread = threading.Thread(target=_post_logs_job, args=())
post_logs_thread.start() post_logs_thread.start()

@ -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 import os
from pathlib import Path
from datetime import datetime from datetime import datetime
import toml import toml
import logging
import click
from copy import deepcopy
import shepherd.scheduler from . import scheduler
import shepherd.config from . import config
import shepherd.plugin from . import plugin
import shepherd.control from . import control
# Future implementations of checking config differences should be done on # 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 # Fix this by saving the working config to /boot when new config applied
# remotely. # remotely.
# Relative pathnames here are all relative to "root_dir"
def define_core_config(confdef): def define_core_config(confdef):
confdef.add_def("id", shepherd.config.StringDef()) """
confdef.add_def("hostname", shepherd.config.StringDef(default="", optional=True)) Defines the config definition by populating the ConfigDefinition passed in ``confdef`` - the same pattern plugins use
confdef.add_def("plugins", shepherd.config.StringArrayDef()) """
confdef.add_def("plugin_dir", shepherd.config.StringDef()) confdef.add_def("name", config.StringDef(
confdef.add_def("root_dir", shepherd.config.StringDef()) helptext="Identifying name for this device"))
confdef.add_def("conf_edit_path", shepherd.config.StringDef())
confdef.add_def("hostname",
confdef.add_def("control_server", shepherd.config.StringDef()) config.StringDef(default="", optional=True,
confdef.add_def("api_key", shepherd.config.StringDef()) helptext="If set, changes the system hostname"))
confdef.add_def("plugin_dir",
def load_config(config_path,load_editconf): config.StringDef(default="~/shepherd-plugins", optional=True,
# Load config from default location helptext="Optional directory for Shepherd to look for plugins in."))
confman = shepherd.config.ConfigManager() confdef.add_def("root_dir",
confman.load(os.path.expanduser(config_path)) 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 # Create core confdef and populate it
core_confdef = shepherd.config.ConfDefinition() core_confdef = config.ConfDefinition()
define_core_config(core_confdef) 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))
(core_conf, plugin_classes, plugin_configs) = load_config(args.configfile, not args.noedit) confman = config.ConfigManager()
confman.add_confdef("shepherd", core_confdef)
if args.test is None: # Default config. This must validate to continue.
shepherd.control.init_control(core_conf, plugin_configs) try:
core_conf, plugin_classes, plugin_configs = load_config_layer(
shepherd.scheduler.init_scheduler(core_conf) confman, Path(default_config_path).expanduser())
logging.info(F"Loaded default config from {default_config_path}")
shepherd.plugin.init_plugins(plugin_classes, plugin_configs, core_conf) 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: if args.test is None:
shepherd.control.post_logs() control.init_control(core_conf, plugin_configs)
shepherd.scheduler.restore_jobs() scheduler.init_scheduler(core_conf)
plugin.init_plugins(plugin_classes, plugin_configs, core_conf)
scheduler.restore_jobs()
print(str(datetime.now())) print(str(datetime.now()))
if args.test is not None: if ctx.invoked_subcommand is not None:
(test_plugin, test_func) = args.test.split(':')
func = getattr(shepherd.plugin.plugin_functions[test_plugin], test_func)
print(func())
return return
print('Press Ctrl+{0} to exit'.format('Break' if os.name == 'nt' else 'C')) print('Press Ctrl+{0} to exit'.format('Break' if os.name == 'nt' else 'C'))
try: try:
shepherd.scheduler.start() scheduler.start()
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):
pass pass
@click.argument('plugin_function')
if __name__ == "__main__": @cli.command()
main() 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

@ -153,9 +153,11 @@ HookAttachment = namedtuple(
class PluginInterface(): class PluginInterface():
# Class to handle the management of a single plugin. '''
# All interaction to or from the plugin to other Shepherd components or Class to handle the management of a single plugin.
# plugins should go through here. All interaction to or from the plugin to other Shepherd components or
plugins should go through here.
'''
def __init__(self, pluginname, pluginclass, pluginconfig, coreconfig): def __init__(self, pluginname, pluginclass, pluginconfig, coreconfig):
if not issubclass(pluginclass, Plugin): 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): 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 = {} plugin_classes = {}
for plugin_name in plugin_names: for plugin_name in plugin_names:
# First look for core plugins, then the plugin_dir, then in the general import path # 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: try:
#mod = importlib.import_module("shepherd.plugins." + plugin_name) #mod = importlib.import_module("shepherd.plugins." + plugin_name)
mod = importlib.import_module('.'+plugin_name, "shepherd.plugins") 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: except ModuleNotFoundError:
try: try:
if (plugin_dir is not None) and (plugin_dir != ""): if (plugin_dir is not None) and (plugin_dir != ""):

@ -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()

@ -8,17 +8,6 @@ import os
import time import time
import argparse 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): class MothtrapPlugin(shepherd.plugin.Plugin):
@ -41,50 +30,48 @@ class MothtrapPlugin(shepherd.plugin.Plugin):
print("Mothtrap config:") print("Mothtrap config:")
print(self.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
self.door_servo = BetterServo(MOTHTRAP_SERVO_PIN, initial_value=None, #servo_max = self.config["servo_open_pulse"] / 1000000
active_high=False, #servo_min = self.config["servo_closed_pulse"] / 1000000
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, #print(F"Supplied min: {servo_min}, max: {servo_max}")
active_high=True,
initial_value=False)
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("usbcam", "pre_cam", self.led_on)
self.interface.attach_hook("picam", "post_cam", self.led_off) self.interface.attach_hook("usbcam", "post_cam", self.led_off)
self.interface.attach_hook("picam", "post_cam", self.run_servo) self.interface.attach_hook("usbcam", "post_cam", self.run_servo)
self.interface.register_function(self.test) self.interface.register_function(self.test)
def led_on(self): def led_on(self):
self.led_power.on() self.plugins["scout"].set_out1(True)
#self.led_power.on()
def led_off(self): def led_off(self):
self.led_power.off() self.plugins["scout"].set_out1(False)
#self.led_power.off()
def run_servo(self): def run_servo(self):
self.door_servo_power.on() self.plugins["scout"].set_aux5v(True)
#self.door_servo_power.on()
time.sleep(0.5) 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"]) 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"]) time.sleep(self.config["servo_open_time"])
self.door_servo.detach() self.plugins["scout"].set_pwm1(False, self.config["servo_closed_pulse"])
self.door_servo_power.off() #self.door_servo.detach()
self.plugins["scout"].set_aux5v(False)
#self.door_servo_power.off()
def test(self): def test(self):
self.led_on() self.led_on()

@ -142,6 +142,7 @@ class USBCamPlugin(shepherd.plugin.Plugin):
self.interface.register_hook("pre_cam") self.interface.register_hook("pre_cam")
self.interface.register_hook("post_cam") self.interface.register_hook("post_cam")
self.interface.register_function(self.camera_job) self.interface.register_function(self.camera_job)
self.interface.register_function(self.run_cameras)
# do some camera init stuff # do some camera init stuff
print("USBCamera config:") print("USBCamera config:")
@ -292,16 +293,14 @@ class USBCamPlugin(shepherd.plugin.Plugin):
print("Could not read camera "+camera_name + print("Could not read camera "+camera_name +
" on USB port "+device_path) " on USB port "+device_path)
def camera_job(self): def run_cameras(self, name_suffix = ""):
self.hooks.pre_cam()
connected_cams = OrderedDict(get_connected_cameras()) connected_cams = OrderedDict(get_connected_cameras())
for defined_name, defined_usb_path in self.defined_cams.items(): for defined_name, defined_usb_path in self.defined_cams.items():
if defined_usb_path in connected_cams: if defined_usb_path in connected_cams:
self._capture_image(connected_cams.pop( self._capture_image(connected_cams.pop(
defined_usb_path), defined_name) defined_usb_path), defined_name+name_suffix)
else: else:
print("USB Camera "+defined_name+" on port " + print("USB Camera "+defined_name+" on port " +
@ -310,12 +309,15 @@ class USBCamPlugin(shepherd.plugin.Plugin):
for cam_name in self.wildcard_cams: for cam_name in self.wildcard_cams:
if len(connected_cams) > 0: if len(connected_cams) > 0:
self._capture_image(connected_cams.popitem( self._capture_image(connected_cams.popitem(
last=False)[1], cam_name) last=False)[1], cam_name+name_suffix)
else: else:
print( print(
"No connected USB cameras are currently left to match to "+cam_name+" ") "No connected USB cameras are currently left to match to "+cam_name+" ")
break break
def camera_job(self):
self.hooks.pre_cam()
self.run_cameras()
self.hooks.post_cam() self.hooks.post_cam()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 954 KiB

@ -1,28 +1,13 @@
[shepherd] [shepherd]
plugin_dir = "~/shepherd/" name = "test-node"
plugins = ["scout"] plugin_dir = "./"
root_dir = "~/shepherd/" root_dir = "~/shepherd/"
conf_edit_path = "~/shepherd.toml"
id = "testnode"
hostname = "shepherd-test" hostname = "shepherd-test"
control_server = "api.shepherd.distreon.net" control_server = "api.shepherd.distreon.net"
#control_server = "127.0.0.1:5000" #control_server = "127.0.0.1:5000"
api_key = "v2EgvYzx79c8fCP4P7jlWxTZ3pc" control_api_key = "v2EgvYzx79c8fCP4P7jlWxTZ3pc"
[scout] [scout]
boardver = "3" boardver = "3"
serialport = "/dev/ttyUSB0" 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"

@ -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)
Loading…
Cancel
Save