def runtest(cfg: Config, args: argparse.Namespace, df: Optional[Datafile]) -> bool: logger = logging.getLogger() # Extract parameters from configuration files try: adc = ADS1115(address=cfg.get_int('Adc', 'Addr'), busnum=cfg.get_int('Adc', 'Bus')) sens = AnalogFlowMeter(adc, cfg.get_int('AnalogFlowSensor', 'Chan'), cfg.get_expr('AnalogFlowSensor', 'Gain'), cfg.get_array('AnalogFlowSensor', 'Coeff')) except BadEntry: logger.exception("Configuration error") return False interval = 1. / args.rate print("Enter ctrl-c to exit ...", file=sys.stderr) sens.reset() try: for tick in ticker(interval): vol, secs = sens.amount() rec = OrderedDict(elapsed=round(secs, 3), vol=round(vol, 3)) writerec(rec) if df: df.add_record("flow", rec, ts=tick) except KeyboardInterrupt: print("done", file=sys.stderr) return True
def quick_validate(cfg: Config): """ Verify that all required configuration entries are present, raises a config.BadEntry exception if any are missing. """ for k, vals in required.items(): for val in vals: cfg.get_string(k, val)
def validate(cfg: Config) -> List[str]: """ Verify that all required configuration entries are present, returns a list of all missing entries. """ missing = [] for k, vals in required.items(): for val in vals: try: cfg.get_string(k, val) except BadEntry: missing.append("/".join([k, val])) return missing
class ValidationTest(unittest.TestCase): def setUp(self): path = os.path.join(os.path.dirname(__file__), "example.cfg") self.cfg = Config(path) self.badcfg = Config(StringIO(INPUT)) def test_validate(self): missing = self.cfg.validate() self.assertEqual(len(missing), 0) def test_validate_fail(self): missing = self.badcfg.validate() self.assertGreater(len(missing), 0)
def main() -> int: args = parse_cmdline() try: cfg = Config(args.syscfg) except Exception as e: print("Error loading system config file; " + str(e), file=sys.stderr) return 1 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO, datefmt='%Y-%m-%d %H:%M:%S', stream=sys.stderr) # eDNA uses the Broadcom SOC pin numbering scheme GPIO.setmode(GPIO.BCM) # If we don't suppress warnings, as message will be printed to stderr # everytime GPIO.setup is called on a pin that isn't in the default # state (input). GPIO.setwarnings(False) status = False try: if args.out: status = runtest(cfg, args, Datafile(args.out)) else: status = runtest(cfg, args, None) except Exception as e: logging.exception("Error running the pump test") finally: if args.clean: GPIO.cleanup() return 0 if status else 1
def main(): parser = argparse.ArgumentParser( description="Validate an eDNA deployment configuration") parser.add_argument("cfg", metavar="FILE", nargs="?", default="", help="deployment configuration file") parser.add_argument( "--sys", metavar="FILE", default=os.path.expanduser("~/.config/edna/system.cfg"), help="location of the system confguration file") args = parser.parse_args() try: cfg = Config(args.sys) except Exception as e: print("Error loading system config file; " + str(e), file=sys.stderr) return 1 if args.cfg: try: cfg.load(args.cfg) except Exception as e: print("Error loading deployment config file; " + str(e), file=sys.stderr) return 1 try: missing = validate(cfg) if missing: print("Missing entries: {}".format(";".join(missing))) return 1 except Exception as e: print("Validation failed: {}".format(str(e)), file=sys.stderr) return 1 return 0
def main() -> int: args = parse_cmdline() try: cfg = Config(args.syscfg) except Exception as e: print("Error loading system config file; " + str(e), file=sys.stderr) return 1 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO, datefmt='%Y-%m-%d %H:%M:%S', stream=sys.stderr) status = False try: if args.out: status = runtest(cfg, args, Datafile(args.out)) else: status = runtest(cfg, args, None) except Exception: logging.exception("Error running the sensor test") return 0 if status else 1
def main() -> int: args = parse_cmdline() try: cfg = Config(args.syscfg) except Exception as e: print("Error loading system config file; " + str(e), file=sys.stderr) return 1 try: cfg.load(args.cfg) except Exception as e: print("Error loading deployment config file; " + str(e), file=sys.stderr) return 1 # Validate configuration files missing = cfg.validate() if missing: print("Missing configuration entries: {}".format(";".join(missing))) return 1 # Generate deployment ID and directory name id = datetime.datetime.now(tz=datetime.timezone.utc).strftime("%Y%m%dT%H%M%S") dir = os.path.join(args.datadir, "edna_" + id) deployment = Deployment(id=id, dir=dir) # Create deployment directory os.makedirs(deployment.dir, exist_ok=True) # Initialize logging init_logging(deployment.dir, deployment.id, debug=args.debug) # Save configuration to deployment directory with open(os.path.join(deployment.dir, "deploy.cfg"), "w") as fp: cfg.write(fp) # Save deployment ID if specified if args.id != "": with open(os.path.join(deployment.dir, "id"), "w") as fp: print(args.id, file=fp) signal.signal(signal.SIGINT, abort) signal.signal(signal.SIGTERM, abort) logger = logging.getLogger() # eDNA uses the Broadcom SOC pin numbering scheme GPIO.setmode(GPIO.BCM) # If we don't suppress warnings, a message will be printed to stderr # everytime GPIO.setup is called on a pin that isn't in the default # state (input). GPIO.setwarnings(False) prfilt: Callable[[float], float] if args.alpha > 0: prfilt = EMA(args.alpha) else: prfilt = lambda x: x status = False try: name = "edna_" + deployment.id + ".ndjson" with open(os.path.join(deployment.dir, name), "w") as fp: status = runedna(cfg, deployment, Datafile(fp), prfilt) except Exception: logger.exception("Deployment aborted with an exception") os.makedirs(args.outbox, exist_ok=True) # Archive the deployment directory to the OUTBOX arpath = os.path.join(args.outbox, "edna_" + deployment.id + ".tar.gz") logger.info("Archiving deployment directory to %s", arpath) with tarfile.open(arpath, "w:gz") as tar: head, tail = os.path.split(deployment.dir) os.chdir(head) tar.add(tail) if args.clean: GPIO.cleanup() return 0 if status else 1
def runedna(cfg: Config, deployment: Deployment, df: Datafile, prfilt: Callable[[float], float]) -> bool: logger = logging.getLogger() logger.info("Starting deployment: %s", deployment.id) if cfg.has_section("Metadata"): meta = [] for key in cfg.options("Metadata"): meta.append(key) meta.append(cfg.get_string("Metadata", key)) df.add_record("metadata", meta) # Extract parameters from configuration files try: pr = dict() adc = ADS1115(address=cfg.get_int('Adc', 'Addr'), busnum=cfg.get_int('Adc', 'Bus')) pr["Filter"] = PrSensor(adc, cfg.get_int('Pressure.Filter', 'Chan'), cfg.get_expr('Pressure.Filter', 'Gain'), coeff=cfg.get_array('Pressure.Filter', 'Coeff')) pr["Env"] = PrSensor(adc, cfg.get_int('Pressure.Env', 'Chan'), cfg.get_expr('Pressure.Env', 'Gain'), coeff=cfg.get_array('Pressure.Env', 'Coeff')) # Discard the first sample psi_to_dbar(pr["Env"].read()) prmax = cfg.get_float('Pressure.Filter', 'Max') def checkpr() -> Tuple[float, bool]: psi = pr["Filter"].read() return psi, psi < prmax def checkdepth(limits: Tuple[float, float]) -> Tuple[float, bool]: dbar = psi_to_dbar(pr["Env"].read()) return prfilt(dbar), limits[0] <= dbar <= limits[1] pumps = dict() for key in ("Sample", "Ethanol"): pumps[key] = Pump(cfg.get_int('Motor.'+key, 'Enable')) valves = dict() for key in ("1", "2", "3", "Ethanol"): vkey = "Valve." + key valves[key] = Valve(cfg.get_int(vkey, 'Enable'), cfg.get_int(vkey, 'IN1'), cfg.get_int(vkey, 'IN2'), lopen=cfg.get_string(vkey, 'open'), lclose=cfg.get_string(vkey, 'close')) fm = AnalogFlowMeter(adc, cfg.get_int('AnalogFlowSensor', 'Chan'), cfg.get_expr('AnalogFlowSensor', 'Gain'), cfg.get_array('AnalogFlowSensor', 'Coeff')) sample_rate = cfg.get_float('FlowSensor', 'Rate') ledctl = LedCtl(obj=LED(cfg.get_int("LED", "GPIO")), fast=cfg.get_float("LED", "fast"), slow=cfg.get_float("LED", "slow"), fade=cfg.get_float("LED", "fade")) limits = dict() for key in ("Sample", "Ethanol"): limits[key] = FlowLimits(amount=cfg.get_float('Collect.'+key, 'Amount'), time=cfg.get_float('Collect.'+key, 'Time')) deployment.seek_err = cfg.get_float('Deployment', 'SeekErr') deployment.depth_err = cfg.get_float('Deployment', 'DepthErr') deployment.pr_rate = cfg.get_float('Deployment', 'PrRate') deployment.seek_time = cfg.get_int('Deployment', 'SeekTime') deployment.downcast = cfg.get_bool('Deployment', 'Downcast') # Each entry in depths is a tuple containing the depth and # the sample index. depths = [] for i, key in enumerate(['Sample.1', 'Sample.2', 'Sample.3']): depths.append((cfg.get_float(key, 'Depth'), i+1)) except BadEntry: logger.exception("Configuration error") return False try: batteries = [Battery(SMBus(0)), Battery(SMBus(1))] except Exception: logger.exception("Battery monitoring disabled") batteries = [] # Samples are collected in depth order, not index order. depths.sort(key=lambda e: e[0], reverse=not deployment.downcast) for target, index in depths: logger.info("Seeking depth for sample %d; %.2f +/- %.2f", index, target, deployment.seek_err) drange = (target-deployment.seek_err, target+deployment.seek_err) with blinker(ledctl.obj, ledctl.slow): depth, status = seekdepth(df, partial(checkdepth, drange), deployment.pr_rate, deployment.seek_time) if not status: logger.critical("Depth seek time limit expired. Aborting.") return False logger.info("Collecting sample %d", index) drange = (depth-deployment.depth_err, depth+deployment.depth_err) status = collect(df, index, (pumps["Sample"], pumps["Ethanol"]), valves, fm, sample_rate, (limits["Sample"], limits["Ethanol"]), checkpr, partial(checkdepth, drange), batteries) return True
def setUp(self): path = os.path.join(os.path.dirname(__file__), "example.cfg") self.cfg = Config(path) self.badcfg = Config(StringIO(INPUT))
def setUp(self): self.cfg = Config(StringIO(INPUT)) self.cfg.load(StringIO(OVERRIDE))
class ConfigTestCase(unittest.TestCase): def setUp(self): self.cfg = Config(StringIO(INPUT)) self.cfg.load(StringIO(OVERRIDE)) def test_get_int(self): x = self.cfg.get_int('Valve.1', 'Enable') self.assertEqual(x, 27) def test_get_int_hex(self): x = self.cfg.get_int('Adc', 'Addr') self.assertEqual(x, 0x48) def test_get_str(self): s = self.cfg.get_string('System', 'LogDir') self.assertEqual(s, "/home/pi/eDNA/logs") def test_get_float(self): x = self.cfg.get_float('Foo', 'Bar') self.assertEqual(x, 2.5) def test_get_array(self): x = self.cfg.get_array('Foo', 'Array') self.assertEqual(x, [5., 6., 7., 8.]) def test_override(self): x = self.cfg.get_int('Foo', 'Baz') self.assertEqual(x, 43) def test_expr(self): x = self.cfg.get_expr('Adc', 'Gain') self.assertEqual(x, 2 / 3) def test_bad_key(self): self.assertRaises(BadEntry, self.cfg.get_int, 'Valve.5', 'Enable') def test_get_bool(self): x = self.cfg.get_bool('Deployment', 'Downcast') self.assertEqual(x, True) def test_bool_default(self): x = self.cfg.get_bool('Foo', 'NotFound') self.assertEqual(x, False)
def main(): parser = argparse.ArgumentParser(description="Prime an eDNA flow path") parser.add_argument("cfg", metavar="FILE", help="system configuration file") parser.add_argument("valve", metavar="N", type=int, help="valve# to prime, 1-3") parser.add_argument("--time", metavar="SECS", type=float, default=30, help="runtime in seconds") parser.add_argument("--prmax", metavar="PSI", type=float, default=12, help="maximum filter pressure in psi") args = parser.parse_args() try: cfg = Config(args.cfg) except Exception as e: print("Error loading config file; " + str(e), file=sys.stderr) sys.exit(1) if args.valve < 1 or args.valve > 3: print("Valve number must be between 1 and 3", file=sys.stderr) sys.exit(1) logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO, datefmt='%Y-%m-%d %H:%M:%S', stream=sys.stderr) # eDNA uses the Broadcom SOC pin numbering scheme GPIO.setmode(GPIO.BCM) try: adc = ADS1115(address=0x48, busnum=cfg.get_int('Adc', 'Bus')) pr_chan = cfg.get_int('Pressure.Filter', 'Chan') pr_gain = cfg.get_float('Pressure.Filter', 'Gain') motor = cfg.get_int('Motor.Sample', 'Enable') GPIO.setup(motor, GPIO.OUT) # Config file key for valve vkey = "Valve." + str(args.valve) sample_valve = Valve(cfg.get_int(vkey, 'Enable'), cfg.get_int(vkey, 'Power'), cfg.get_int(vkey, 'Gnd')) eth_valve = Valve(cfg.get_int('Valve.Ethanol', 'Enable'), cfg.get_int('Valve.Ethanol', 'Power'), cfg.get_int('Valve.Ethanol', 'Gnd')) except BadEntry as e: print(str(e), file=sys.stderr) GPIO.cleanup() sys.exit(2) try: prime_cycle(sample_valve, eth_valve, motor, partial(checkpr, adc, pr_chan, pr_gain, args.prmax), args.time) finally: GPIO.cleanup()
def runtest(cfg: Config, args: argparse.Namespace, df: Optional[Datafile]) -> bool: logger = logging.getLogger() # Extract parameters from configuration files try: sens = dict() adc = ADS1115(address=cfg.get_int('Adc', 'Addr'), busnum=cfg.get_int('Adc', 'Bus')) sens["Filter"] = PrSensor(adc, cfg.get_int('Pressure.Filter', 'Chan'), cfg.get_expr('Pressure.Filter', 'Gain')) prmax = cfg.get_float('Pressure.Filter', 'Max') def checkpr() -> Tuple[float, bool]: psi = sens["Filter"].read() return psi, psi < prmax pumps = dict() for key in ("Sample", "Ethanol"): pumps[key.lower()] = Pump(cfg.get_int('Motor.' + key, 'Enable')) valves = dict() for key in ("1", "2", "3", "Ethanol"): vkey = "Valve." + key valves[key.lower()] = Valve(cfg.get_int(vkey, 'Enable'), cfg.get_int(vkey, 'IN1'), cfg.get_int(vkey, 'IN2'), lopen=cfg.get_string(vkey, 'open'), lclose=cfg.get_string(vkey, 'close')) fm = AnalogFlowMeter(adc, cfg.get_int('AnalogFlowSensor', 'Chan'), cfg.get_expr('AnalogFlowSensor', 'Gain'), cfg.get_array('AnalogFlowSensor', 'Coeff')) except BadEntry: logger.exception("Configuration error") return False if args.pump not in pumps: logger.critical("Invalid pump name: '%s'", args.pump) return False if args.valve not in valves: logger.critical("Invalid valve: '%s'", args.valve) return False interval = 1. / args.rate print("Enter ctrl-c to exit ...", file=sys.stderr) try: fm.reset() with valves[args.valve]: with pumps[args.pump]: for tick in ticker(interval): amount, secs = fm.amount() pr, pr_ok = checkpr() rec = OrderedDict(elapsed=round(secs, 3), vol=round(amount, 3), pr=round(pr, 3)) writerec(rec) if df: df.add_record("flow", rec, ts=tick) except KeyboardInterrupt: print("done", file=sys.stderr) return True