From baeb1ab185f00889ec7f33896bf25bfc4e5a16cc Mon Sep 17 00:00:00 2001 From: lhahn Date: Sat, 19 Aug 2023 23:37:34 +0200 Subject: [PATCH] Git initial commit --- .gitignore | 32 ++ Info.txt | 9 + LICENSE | 21 ++ README.md | 18 ++ bin/.gitkeep | 0 lib/python3/green_environment/__init__.py | 2 + lib/python3/green_environment/ds18b20.py | 372 ++++++++++++++++++++++ lib/python3/green_environment/giesomat.py | 289 +++++++++++++++++ makefile | 0 setup.py | 21 ++ src/.gitkeep | 0 11 files changed, 764 insertions(+) create mode 100755 .gitignore create mode 100755 Info.txt create mode 100755 LICENSE create mode 100755 README.md create mode 100755 bin/.gitkeep create mode 100755 lib/python3/green_environment/__init__.py create mode 100755 lib/python3/green_environment/ds18b20.py create mode 100755 lib/python3/green_environment/giesomat.py create mode 100755 makefile create mode 100755 setup.py create mode 100755 src/.gitkeep diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..259148f --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app diff --git a/Info.txt b/Info.txt new file mode 100755 index 0000000..c116150 --- /dev/null +++ b/Info.txt @@ -0,0 +1,9 @@ +Measuring soil moisture requires to translate frequency to a certain water level. +The easiest one is to set minimum and maximum of water to a certain frequency. + +This was done experimental; in future, there will be a calibration function, that tracks the optimal values aswell. + +Currently we use: + +- "Air with 40% humidity" ~ +-9350 HZ +- "Sensor complete in water" ~ +- 800 HZ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..81dee20 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Lars Hahn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100755 index 0000000..bdef228 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# green-environment + +The intention of this project is to setup an application which can be used within greenhouses, that monitors environmental factors and partially is able to controle these. + +The core of the project is to have a set of plants, where soil moisture sensors are integrated, next to sensors for air pressure, humidity, light intensity and temperature (gas meter are an interesseting aspect for the future aswell!). + +With this application we then can for instance controll a pump to control irrigation. + +The first step is to implement a short commandline application in python, followed by a C++ application, which is/are then extended with a GUI and a live plotting of current values (temp etc.). + + +The hardware base will be a raspberry pi for sensor access. +The following sensors will be used: +- Gies-O-Mat soil moisture sensor from ramser-elektro.at is used +- TSL2591 Lux Sensor (or alternative) will be used for light parameter +- BME280 Pressure, Temperature, Humidity Sensor for air related parameters +- DS18B20 Waterproof temperature sensor for plant soil temperature + diff --git a/bin/.gitkeep b/bin/.gitkeep new file mode 100755 index 0000000..e69de29 diff --git a/lib/python3/green_environment/__init__.py b/lib/python3/green_environment/__init__.py new file mode 100755 index 0000000..3833bca --- /dev/null +++ b/lib/python3/green_environment/__init__.py @@ -0,0 +1,2 @@ +from giesomat import GiesOMat +from ds18b20 import DS18B20 \ No newline at end of file diff --git a/lib/python3/green_environment/ds18b20.py b/lib/python3/green_environment/ds18b20.py new file mode 100755 index 0000000..d86764e --- /dev/null +++ b/lib/python3/green_environment/ds18b20.py @@ -0,0 +1,372 @@ +#!/usr/bin/env python3 +""" +This module provides a class to interact with the DS18B20 temperature +sensor and is considered mostly to be used on a Raspberry Pi. With +aprorpiate naming this class might be used for other SoC like Arduino, +Genuino etc. + +This module can also be used as a standalone script to retrieve values from. +attached sensors. + +Classes: DS18B20 +Functions: main +""" +import os +import argparse +import time + +os.system('modprobe w1-gpio') +os.system('modprobe w1-therm') + +class DS18B20: + """ + This class provides a set of functions and methods that can be used to + interact with the DS18B20 sensor. As values can be set/adjusted during + runtime, there is no need to instantiate a new object for every config + change. Multiple sensors can be handled at once by passing a list of + devices or using None (take all available devices). + + Methods: + default_functor(values, **kwargs) + __init__(device, scale, idle_time) + set_devices() + devices() + set_device_path() + device_path() + set_scale() + scale() + set_idle_time() + idle_time() + __to_celsius + __to_kelvin + __to_fahrenheit + get(iteration) + run(iteration, functor, **kwargs) + run_endless(iteration, functor, **kwargs) + """ + def default_functor(values: "list of ints" or int, **kwargs): + """ + An example of how functions can be passed to the run function, + to make use of a value handling (e.g. printing values). + + Keyword arguments: + values (list of ints) -- the measured values for one run. + Args: + **kwargs -- arguments that can be evaluated in another function. + """ + if not isinstance(values, list): + print(values, **kwargs) + else: + print("\t".join(str(value) for value in values), **kwargs) + + + def __init__(self, device, scale: str = "C", idle_time: int = 2): + """ + Constructor to instantiate an object, that is able to handle multiple + devices at once. + + Keyword arguments: + device (str, list of str) -- The 1-wire devices that should be used. + scale (str, optional) -- The temperature scalce (K,F,C) to be used + idle_time (int, optional) -- The idle time between two measurements. + """ + self.set_scale(scale) + self._idle_time = idle_time + self._device_path = "/sys/bus/w1/devices" + dev = [device] if not isinstance(device,list) else device + self.set_devices(dev) + + + + def set_devices(self, devices: list=None): + """ + This functions loads the required 1-wire devices. + If no device list is provided, all available devices will be used. + + Keyword arguments: + devices (list of str) -- the devices to be used; leave empty + to use all devices + + """ + device_list = [devices] if not isinstance(devices, list) else devices + # filter available 1-wire devices # + one_wire_devices = [ + dev.name for dev in os.scandir(self._device_path) + if dev.is_dir() and dev.name != "w1_bus_master1" + ] + + # filter for active 1-wire devices # + self._devices = [ + dev for dev in one_wire_devices + if "w1_slave" in ( + item.name for item in os.scandir("{}/{}".format(self._device_path, dev)) + ) + ] + + # reduce to chosen, active devices # + if device_list != [None]: + self._devices = [ + device + for device in self._devices + if device in device_list + ] + if len(self._devices) != len(device_list): + missing_devices = [ + dev for dev in devices if dev not in self._devices + ] + raise FileNotFoundError( + "Provided devices [{}] cannot be found! Aborting.".format( + ", ".join(missing_devices) + ) + ) + if not self._devices: + raise FileNotFoundError("No 1-wire device found!") + + + def devices(self): + """ + The function to get (by return) the current device list in use. + + Returns: + (list) -- The device list. + """ + return list(self._devices) + + + def set_device_path(self, device_path: str): + """ + Sets the device path, were 1-wire devices can be found. + Usually it is '/sys/bus/w1/devices' + + Keyword arguments: + device_path (str) -- The device path, e.g. /sys/bus/w1/devices + """ + self._device_path = device_path + + + def device_path(self): + """ + The function to get (by return) the current device_path, where + 1-wire devices should be found. + + Returns: + (str) -- The device_path as a string. + """ + return self._device_path + + + def set_scale(self, scale: str): + """ + Sets the temperature scale, that should be used; it is either + 'K', 'F' or 'C'; relates to Kevlin, Fahrenheit or Celsius. + Returned values are to be read with that scale; default is C. + + Keyword arguments: + scale (str) -- The temperature scale: 'K', 'F' or 'C'. + """ + if scale not in ("K", "F", "C"): + raise ValueError( + "Unknown scale type '{}'! Needs to be K, F or C!".format(scale) + ) + self._scale = scale + if self._scale == "C": + self._translator = self.__to_celsius + elif self._scale == "F": + # Fahrenheit temperature scale # + self._translator = self.__to_fahrenheit + else: + # Kelvin temperature scale # + self._translator = self.__to_kelvin + + + def scale(self): + """ + The function to get (by return) the current used temperature scale. + This is either 'K', 'F' or 'C'. + + Returns: + (str) -- The uses temperature scale as a string. + """ + return self._scale + + + def set_idle_time(self, idle_time: int): + """ + Sets the idle time that is used in between two + measurements when using run(...) or run_endless(...) + + Keyword arguments: + idle_time (int) -- The idle time. + """ + self._idle_time = idle_time + + + def idle_time(self): + """ + The function to get (by return) the current used idle time. + + Returns: + (int) -- The currently used idle time. + """ + return self._idle_time + + + def __to_celsius(self, value: int): + """ + Transforms measured value into Celcius temperature scale. + + Keyword arguments: + value (int) -- A measured value that should be transformed. + + Returns: + (int) -- The transformed input value. + """ + return value / 1000.0 + + + def __to_kelvin(self, value: int): + """ + Transforms measured value into Kelvin temperature scale. + + Keyword arguments: + value (int) -- A measured value that should be transformed. + + Returns: + (int) -- The transformed input value. + """ + return (value/1000) + 273.15 + + + def __to_fahrenheit(self, value: int): + """ + Transforms measured value into Fahrenheit temperature scale. + + Keyword arguments: + value (int) -- A measured value that should be transformed. + + Returns: + (int) -- The transformed input value. + """ + return (value/1000)*(9/5) + 32 + + + def get(self, iteration: int = 1): + """ + Function to get a certain amount of measured values; there is no + on-line handling. Values will be measured and returned. + + Keyword arguments: + iteration (int, optional) -- Measured values amount. Defaults to 1. + + Returns: + (list of list of ints or list of ints) -- The measured values + """ + iteration_values = [] + for _ in range(iteration): + current_temp = [] + for device in self._devices: + one_wire_file = "{}/{}/w1_slave".format(self._device_path, device) + with open(one_wire_file, "r") as bytestream: + data = [ + item.rstrip().split(" ")[-1] for item in bytestream.readlines() + ] + if data[0] == "YES": + current_temp.append(self._translator(int(data[1][2:]))) + else: + current_temp.append(None) + iteration_values.append(current_temp) + time.sleep(self._idle_time) + if iteration == 1: + return iteration_values[0] + return iteration_values + + + def run(self, iteration: int = 1, functor=default_functor, **kwargs): + """ + Function to measure a certain amount of values and evaluate them directly. + Evaluation can be a print function or a self-defined function. + Options can be passed with **kwargs. + + Keyword arguments: + iteration (int, optional) -- Number of measurements to be done. + Defaults to 1. + functor (function_ptr, optional) -- An evaluationfunction. + Defaults to default_functor. + Args: + **kwargs -- arguments that can be evaluated in another function. + """ + while iteration != 0: + if iteration > 0: + iteration -= 1 + current_temp = [] + for device in self._devices: + one_wire_file = "{}/{}/w1_slave".format(self._device_path, device) + with open(one_wire_file, "r") as bytestream: + data = [ + item.rstrip().split(" ")[-1] for item in bytestream.readlines() + ] + if data[0] == "YES": + current_temp.append(self._translator(int(data[1][2:]))) + else: + current_temp.append(None) + functor(current_temp, **kwargs) + time.sleep(self._idle_time) + + + def run_endless(self, functor=default_functor, **kwargs): + """ + Function to permanently measure values and evaluate them directly. + Evaluation can be a print function or a self-defined function. + Options can be passed with **kwargs. + + Keyword arguments: + functor (function_ptr, optional) -- An evaluationfunction. + Defaults to default_functor. + Args: + **kwargs -- arguments that can be evaluated in another function. + """ + self.run(iteration=-1, functor=functor, **kwargs) + + +def main(): + """ + A main function that is used, when this module is used as a stand-alone script. + Arguments can be passed and it will simply print results to std-out. + """ + parser = argparse.ArgumentParser( + description="A short programm to print values from DS18B20 sensor." + ) + parser.add_argument( + "-d", metavar="D", nargs="+", type=str, required=True, + help="The devices 1-wire, that should be used to measure temperature." + ) + parser.add_argument( + "-t", metavar="T", default=2, type=int, required=False, + help="Set idle time (break between measurements); default t = 2." + ) + parser.add_argument( + "-c", metavar="C", default="C", type=str, required=False, + help="The tempearute scale to use (K,F,C); default c = C." + ) + parser.add_argument( + "-i", metavar="I", default=10, type=int, required=False, + help="Number of iterations to get a value; use -1 for infinity." + ) + + args = parser.parse_args() + devices = args.d + iterations = -1 if args.i < 0 else args.i + idle_time = args.t + scale = args.c + + connector = DS18B20( + device=devices, + scale=scale, + idle_time=idle_time + ) + connector.run(iterations) + + +if __name__ == "__main__": + # execute only if run as a script + main() \ No newline at end of file diff --git a/lib/python3/green_environment/giesomat.py b/lib/python3/green_environment/giesomat.py new file mode 100755 index 0000000..6e60f7b --- /dev/null +++ b/lib/python3/green_environment/giesomat.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +""" +This module provides a class to interact with the Gies-O-Mat soil moisture +sensor from ramser-elektro.at and is considered mostly to be used on a +Raspberry Pi. With aprorpiate naming and if pigpio is also available, +this class might be used for other SoC like Arduino, Genuino etc. + +This module can also be used as a standalone script to retrieve values from. +attached sensors. + +Classes: GiesOMat +Functions: main +""" +import argparse +import time +import pigpio + + + +class GiesOMat: + """ + This class provides a set of functions and methods that can be used to + interact with the Gies-O-Mat sensor. As values can be set/adjusted during + runtime, there is no need to instantiate a new object for every config + change. Multiple sensors can be handled at once by passing a list of + GPIO pins. + + Methods: + default_functor(values, **kwargs) + __init__ + set_gpio(gpio) + gpio() + set_pulse(pulse) + pulse() + set_sample_rate(sample_rate) + sample_rate() + set_callback(call_back_id) + callback() + reset() + get(iteration) + run(iteration, functor, **kwargs) + run_endless(iteration, functor, **kwargs) + """ + def default_functor(values: list or int, **kwargs): + """ + An example of how functions can be passed to the run function, + to make use of a value handling (e.g. printing values). + + Keyword arguments: + values (list of ints) -- the measured values for one run. + Args: + **kwargs -- arguments that can be evaluated in another function + """ + if not isinstance(values, list): + print(values, **kwargs) + else: + print("\t".join(str(value) for value in values), **kwargs) + + def __init__( + self, gpio, pulse=20, sample_rate=5, + call_back_id=pigpio.RISING_EDGE): + """ + Constructor to instantiate an object, that is able to handle multiple + sensors at once. + + Keyword arguments: + gpio (int, list of ints) -- GPIO pins that are connected to 'OUT' + pulse (int, optional) -- The time for the charging wave. Defaults to 20. + sample_rate (int, optional) -- Time span how long to count switches. Defaults to 5. + call_back_id (int, optional) -- Callback id. Defaults to pigpio.RISING_EDGE. + """ + self.set_gpio(gpio) + self.set_pulse(pulse) + self.set_sample_rate(sample_rate) + self.set_callback(call_back_id) + self._pin_mask = 0 + self.reset() + + def set_gpio(self, gpio: list or int): + """ + The function allows to set or update the GPIO pin list of a GiesOMat + instance, so that pins can be changed/updated. + + Keyword arguments: + gpio (int, list of ints) -- Sensor pins that are connected to "OUT". + """ + self._gpio = gpio if isinstance(gpio, list) else [gpio] + + def gpio(self): + """ + The function to get (by return) the current list of used GPIO pins + where data is taken from. + + Returns: + (list of ints) -- Returns the list of used GPIO pins. + """ + return [gpio_pin for gpio_pin in self._gpio] + + def set_pulse(self, pulse: int): + """ + Sets the pulse value (in µs) to the instance and on runtime. + + Keyword arguments: + pulse (int) -- The pulse value in µs. + """ + self._pulse = pulse + + def pulse(self): + """ + The function to get (by return) the current pulse value. + + Returns: + (int) -- The currently used pulse value. + """ + return self._pulse + + def set_sample_rate(self, sample_rate: int or float): + """ + Sets the sample_rate value (in deciseconds [10^-1 s])to the instance + and on runtime. + + Keyword arguments: + sample_rate (int) -- The sample_rate value in deciseconds. + """ + self._sample_rate = sample_rate + + def sample_rate(self): + """ + The function to get (by return) the current sample_rate value. + + Returns: + (int) -- The currently used sample_rate value. + """ + return self._sample_rate + + def set_callback(self, call_back_id: int): + """ + Sets the used callback trigger (when to count, rising, falling switch + point). + + Keyword arguments: + call_back_id (int) -- The callback id, e.g. pigpio.RISING_EDGE. + """ + self._call_back_id = call_back_id + + def callback(self): + """ + The function to get (by return) the current callback_id value. + + Returns: + (int) -- The callback_id, please relate to e.g. pigpio.RISING_EDGE. + """ + return self._call_back_id + + def reset(self): + """ + This functions resets all necessary runtime variables, so that after + a configuration change, everything is correctly loaded. + """ + self._pi = pigpio.pi() + self._pi.wave_clear() + for gpio_pin in self._gpio: + self._pin_mask |= 1 << gpio_pin + self._pi.set_mode(gpio_pin, pigpio.INPUT) + self._pulse_gpio = [ + pigpio.pulse(self._pin_mask, 0, self._pulse), + pigpio.pulse(0, self._pin_mask, self._pulse) + ] + self._pi.wave_add_generic(self._pulse_gpio) + self._wave_id = self._pi.wave_create() + self._call_backs = [ + self._pi.callback( + gpio_pin, self._call_back_id + ) for gpio_pin in self._gpio + ] + for callback in self._call_backs: + callback.reset_tally() + + def get(self, iteration: int = 1): + """ + Function to get a certain amount of measured values; there is no + on-line handling. Values will be measured and returned. + + Keyword arguments: + iteration (int, optional) -- Measured values amount. Defaults to 1. + + Returns: + (list of list of ints or list of ints) -- The measured values + """ + # initialise/reset once to have values with beginning! + for call in self._call_backs: + call.reset_tally() + time.sleep(0.1 * self._sample_rate) + iteration_values = [] + for _ in range(iteration): + values = [call.tally() for call in self._call_backs] + iteration_values.append(values) + for call in self._call_backs: + call.reset_tally() + time.sleep(0.1 * self._sample_rate) + if iteration == 1: + return iteration_values[0] + return iteration_values + + def run(self, iteration: int = 1, functor=default_functor, **kwargs): + """ + Function to measure a certain amount of values and evaluate them directly. + Evaluation can be a print function or a self-defined function. + Options can be passed with **kwargs. + + Keyword arguments: + iteration (int, optional) -- Number of measurements to be done. + Defaults to 1. + functor (function_ptr, optional) -- An evaluationfunction. + Defaults to default_functor. + Args: + **kwargs -- arguments that can be evaluated in another function. + """ + # initialise/reset once to have values with beginning! + for call in self._call_backs: + call.reset_tally() + time.sleep(0.1 * self._sample_rate) + while iteration != 0: + if iteration > 0: + iteration -= 1 + values = [call.tally() for call in self._call_backs] + functor(values, **kwargs) + for call in self._call_backs: + call.reset_tally() + time.sleep(0.1 * self._sample_rate) + + def run_endless(self, functor=default_functor, **kwargs): + """ + Function to permanently measure values and evaluate them directly. + Evaluation can be a print function or a self-defined function. + Options can be passed with **kwargs. + + Keyword arguments: + functor (function_ptr, optional) -- An evaluationfunction. + Defaults to default_functor. + Args: + **kwargs -- arguments that can be evaluated in another function. + """ + self.run(iteration=-1, functor=functor, **kwargs) + + +def main(): + """ + A main function that is used, when this module is used as a stand-alone script. + Arguments can be passed and it will simply print results to std-out. + """ + parser = argparse.ArgumentParser( + description="A short programm to print values from Gies-O-Mat sensor." + ) + parser.add_argument( + "-g", metavar="G", nargs="+", type=int, required=True, + help="GPIO pin number(s), where the OUT sensor(s) pin is/are attached to." + ) + parser.add_argument( + "-p", metavar="P", default=20, type=int, required=False, + help="Set Pulse to P µs, default p = 20µs." + ) + parser.add_argument( + "-s", metavar="S", default=5, type=int, required=False, + help="Set sample rate to S deciseconds [10^-1 s]; default s = 5." + ) + parser.add_argument( + "-i", metavar="I", default=10, type=int, required=False, + help="Number of iterations to get a value; use -1 for infinity." + ) + + args = parser.parse_args() + + gpio_pins = args.g + iterations = -1 if args.i < 0 else args.i + pulse = args.p + sample_rate = args.s + + connector = GiesOMat( + gpio=gpio_pins, + pulse=pulse, + sample_rate=sample_rate, + ) + connector.run(iterations) + + +if __name__ == "__main__": + # execute only if run as a script + main() diff --git a/makefile b/makefile new file mode 100755 index 0000000..e69de29 diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..e64cbd6 --- /dev/null +++ b/setup.py @@ -0,0 +1,21 @@ +import setuptools + +with open("README.md", "r") as file: + program_description = file.read() + +setuptools.setup( + name="green_environment", + version="0.0.0", + author="Lars Hahn", + author_email="lhahn@data-learning.de", + description="Package to setup an application and libraries concerning environmental sensors and plant irrigation", + long_description=program_description, + long_description_content_type="text/markdown", + packages=setuptools.find_packages(), + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + python_requires='>=3.7', +) \ No newline at end of file diff --git a/src/.gitkeep b/src/.gitkeep new file mode 100755 index 0000000..e69de29