Git initial commit

This commit is contained in:
Lars Hahn 2023-08-19 23:37:34 +02:00
commit baeb1ab185
11 changed files with 764 additions and 0 deletions

32
.gitignore vendored Executable file
View File

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

9
Info.txt Executable file
View File

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

21
LICENSE Executable file
View File

@ -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.

18
README.md Executable file
View File

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

0
bin/.gitkeep Executable file
View File

View File

@ -0,0 +1,2 @@
from giesomat import GiesOMat
from ds18b20 import DS18B20

View File

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

View File

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

0
makefile Executable file
View File

21
setup.py Executable file
View File

@ -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',
)

0
src/.gitkeep Executable file
View File