Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 23 additions & 18 deletions grow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,52 @@
import threading
import time

import RPi.GPIO as GPIO
import gpiodevice

from . import pwm

PLATFORMS = {
"Raspberry Pi 5": {"piezo": ("PIN33", pwm.OUTL)},
"Raspberry Pi 4": {"piezo": ("GPIO13", pwm.OUTL)},
}


class Piezo():
def __init__(self, gpio_pin=13):
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(gpio_pin, GPIO.OUT, initial=GPIO.LOW)
self.pwm = GPIO.PWM(gpio_pin, 440)
self.pwm.start(0)
def __init__(self, gpio_pin=None):

if gpio_pin is None:
gpio_pin = gpiodevice.get_pins_for_platform(PLATFORMS)[0]
elif isinstance(gpio_pin, str):
gpio_pin = gpiodevice.get_pin(gpio_pin, "piezo", pwm.OUTL)

self.pwm = pwm.PWM(gpio_pin)
self._timeout = None
atexit.register(self._exit)
pwm.PWM.start_thread()
atexit.register(pwm.PWM.stop_thread)

def frequency(self, value):
"""Change the piezo frequency.

Loosely corresponds to musical pitch, if you suspend disbelief.

"""
self.pwm.ChangeFrequency(value)
self.pwm.set_frequency(value)

def start(self, frequency=None):
def start(self, frequency):
"""Start the piezo.

Sets the Duty Cycle to 100%
Sets the Duty Cycle to 50%

"""
if frequency is not None:
self.frequency(frequency)
self.pwm.ChangeDutyCycle(1)
self.pwm.start(frequency=frequency, duty_cycle=0.5)

def stop(self):
"""Stop the piezo.

Sets the Duty Cycle to 0%

"""
self.pwm.ChangeDutyCycle(0)
self.pwm.stop()

def beep(self, frequency=440, timeout=0.1, blocking=True, force=False):
"""Beep the piezo for time seconds.
Expand All @@ -67,6 +75,3 @@ def beep(self, frequency=440, timeout=0.1, blocking=True, force=False):
self.start(frequency=frequency)
self._timeout.start()
return True

def _exit(self):
self.pwm.stop()
71 changes: 41 additions & 30 deletions grow/moisture.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import atexit
import select
import threading
import time
from datetime import timedelta

import RPi.GPIO as GPIO
import gpiodevice
from gpiod import LineSettings
from gpiod.line import Bias, Edge

MOISTURE_1_PIN = 23
MOISTURE_2_PIN = 8
Expand All @@ -25,46 +31,55 @@ def __init__(self, channel=1, wet_point=None, dry_point=None):
"""
self._gpio_pin = [MOISTURE_1_PIN, MOISTURE_2_PIN, MOISTURE_3_PIN, MOISTURE_INT_PIN][channel - 1]

GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(self._gpio_pin, GPIO.IN)

self._count = 0
self._reading = 0
self._history = []
self._history_length = 200
self._last_pulse = time.time()
self._new_data = False
self._wet_point = wet_point if wet_point is not None else 0.7
self._dry_point = dry_point if dry_point is not None else 27.6
self._time_last_reading = time.time()
try:
GPIO.add_event_detect(self._gpio_pin, GPIO.RISING, callback=self._event_handler, bouncetime=1)
except RuntimeError as e:
if self._gpio_pin == 8:
raise RuntimeError("""Unable to set up edge detection on BCM8.

Please ensure you add the following to /boot/config.txt and reboot:
self._last_reading = 0

self._int, self._offset = gpiodevice.get_pin(self._gpio_pin, f"grow-m-ch{channel}", LineSettings(
edge_detection=Edge.RISING, bias=Bias.PULL_DOWN, debounce_period=timedelta(milliseconds=10)
))

self._poll_thread_event = threading.Event()
self._poll_thread = threading.Thread(target=self._thread_poll)
self._poll_thread.start()

dtoverlay=spi0-cs,cs0_pin=14 # Re-assign CS0 from BCM 8 so that Grow can use it
atexit.register(self._thread_stop)

""")
else:
raise e
def __del__(self):
self._thread_stop()

self._time_start = time.time()
def _thread_stop(self):
self._poll_thread_event.set()
self._poll_thread.join()

def _event_handler(self, pin):
self._count += 1
self._last_pulse = time.time()
if self._time_elapsed >= 1.0:
self._reading = self._count / self._time_elapsed
def _thread_poll(self):
poll = select.poll()
try:
poll.register(self._int.fd, select.POLLIN)
except TypeError:
return
while not self._poll_thread_event.wait(1.0):
if not poll.poll(0):
# No pulses in 1s, this is not a valid reading
continue

# We got pulses, since we waited for 1s we can be relatively
# sure the number of pulses is approximately the sensor frequency
events = self._int.read_edge_events()
self._last_reading = time.time()
self._reading = len(events) # Pulse frequency
self._history.insert(0, self._reading)
self._history = self._history[:self._history_length]
self._count = 0
self._time_last_reading = time.time()
self._new_data = True

poll.unregister(self._int.fd)

@property
def history(self):
history = []
Expand All @@ -76,10 +91,6 @@ def history(self):

return history

@property
def _time_elapsed(self):
return time.time() - self._time_last_reading

def set_wet_point(self, value=None):
"""Set the sensor wet point.

Expand Down Expand Up @@ -123,7 +134,7 @@ def moisture(self):
@property
def active(self):
"""Check if the moisture sensor is producing a valid reading."""
return (time.time() - self._last_pulse) < 1.0 and self._reading > 0 and self._reading < 28
return (time.time() - self._last_reading) <= 1.0 and self._reading > 0 and self._reading < 28

@property
def new_data(self):
Expand Down
40 changes: 23 additions & 17 deletions grow/pump.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@
import threading
import time

import RPi.GPIO as GPIO
import gpiodevice

PUMP_1_PIN = 17
PUMP_2_PIN = 27
PUMP_3_PIN = 22
from . import pwm

PUMP_1_PIN = "PIN11" # 17
PUMP_2_PIN = "PIN13" # 27
PUMP_3_PIN = "PIN15" # 22
PUMP_PWM_FREQ = 10000
PUMP_MAX_DUTY = 90
PUMP_MAX_DUTY = 0.9

PLATFORMS = {
"Raspberry Pi 5": {"pump1": ("PIN11", pwm.OUTL), "pump2": ("PIN12", pwm.OUTL), "pump3": ("PIN15", pwm.OUTL)},
"Raspberry Pi 4": {"pump1": ("GPIO17", pwm.OUTL), "pump2": ("GPIO27", pwm.OUTL), "pump3": ("GPIO22", pwm.OUTL)},
}


global_lock = threading.Lock()
Expand All @@ -17,6 +24,8 @@
class Pump(object):
"""Grow pump driver."""

PINS = None

def __init__(self, channel=1):
"""Create a new pump.

Expand All @@ -26,21 +35,18 @@ def __init__(self, channel=1):

"""

self._gpio_pin = [PUMP_1_PIN, PUMP_2_PIN, PUMP_3_PIN][channel - 1]
if Pump.PINS is None:
Pump.PINS = gpiodevice.get_pins_for_platform(PLATFORMS)

GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(self._gpio_pin, GPIO.OUT, initial=GPIO.LOW)
self._pwm = GPIO.PWM(self._gpio_pin, PUMP_PWM_FREQ)
self._pwm.start(0)
self._gpio_pin = Pump.PINS[channel - 1]

self._timeout = None
self._pwm = pwm.PWM(self._gpio_pin, PUMP_PWM_FREQ)
self._pwm.start(0)

atexit.register(self._stop)
pwm.PWM.start_thread()
atexit.register(pwm.PWM.stop_thread)

def _stop(self):
self._pwm.stop(0)
GPIO.setup(self._gpio_pin, GPIO.IN)
self._timeout = None

def set_speed(self, speed):
"""Set pump speed (PWM duty cycle)."""
Expand All @@ -52,7 +58,7 @@ def set_speed(self, speed):
elif not global_lock.acquire(blocking=False):
return False

self._pwm.ChangeDutyCycle(int(PUMP_MAX_DUTY * speed))
self._pwm.set_duty_cycle(PUMP_MAX_DUTY * speed)
self._speed = speed
return True

Expand Down
105 changes: 105 additions & 0 deletions grow/pwm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import time
from threading import Event, Thread

import gpiod
import gpiodevice
from gpiod.line import Direction, Value

OUTL = gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE)


class PWM:
_pwms: list = []
_t_pwm: Thread = None
_t_pwm_event: Event = Event()

@staticmethod
def start_thread():
if PWM._t_pwm is None:
PWM._t_pwm = Thread(target=PWM._run)
PWM._t_pwm.start()

@staticmethod
def stop_thread():
if PWM._t_pwm is not None:
PWM._t_pwm_event.set()
PWM._t_pwm.join()
PWM._t_pwm = None

@staticmethod
def _add(pwm):
PWM._pwms.append(pwm)

@staticmethod
def _remove(pwm):
index = PWM._pwms.index(pwm)
PWM._pwms.pop(index)
if len(PWM._pwms) == 0:
PWM.stop_thread()

@staticmethod
def _run():
while not PWM._t_pwm_event.is_set():
PWM.run()

@staticmethod
def run():
for pwm in PWM._pwms:
pwm.next(time.time())

def __init__(self, pin, frequency=0, duty_cycle=0, lines=None, offset=None):
self.duty_cycle = 0
self.frequency = 0
self.duty_period = 0
self.period = 0
self.running = False
self.time_start = None
self.state = Value.ACTIVE

self.set_frequency(frequency)
self.set_duty_cycle(duty_cycle)

if isinstance(pin, tuple):
self.lines, self.offset = pin
else:
self.lines, self.offset = gpiodevice.get_pin(pin, "PWM", OUTL)

PWM._add(self)

def set_frequency(self, frequency):
if frequency == 0:
return
self.frequency = frequency
self.period = 1.0 / frequency
self.duty_period = self.duty_cycle * self.period

def set_duty_cycle(self, duty_cycle):
self.duty_cycle = duty_cycle
self.duty_period = self.duty_cycle * self.period

def start(self, duty_cycle=None, frequency=None, start_time=None):
if duty_cycle is not None:
self.set_duty_cycle(duty_cycle)

if frequency is not None:
self.set_frequency(frequency)

self.time_start = time.time() if start_time is None else start_time

self.running = True

def next(self, t):
if not self.running:
return
d = t - self.time_start
d %= self.period
new_state = Value.ACTIVE if d < self.duty_period else Value.INACTIVE
if new_state != self.state:
self.lines.set_value(self.offset, new_state)
self.state = new_state

def stop(self):
self.running = False

def __del__(self):
PWM._remove(self)
4 changes: 2 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ DATESTAMP=$(date "+%Y-%m-%d-%H-%M-%S")
CONFIG_BACKUP=false
APT_HAS_UPDATED=false
RESOURCES_TOP_DIR=$HOME/Pimoroni
VENV_BASH_SNIPPET=$RESOURCES_DIR/auto_venv.sh
VENV_BASH_SNIPPET=$RESOURCES_TOP_DIR/auto_venv.sh
VENV_DIR=$HOME/.virtualenvs/pimoroni
WD=$(pwd)
USAGE="./install.sh (--unstable)"
Expand Down Expand Up @@ -77,7 +77,7 @@ find_config() {
venv_bash_snippet() {
if [ ! -f "$VENV_BASH_SNIPPET" ]; then
cat << EOF > "$VENV_BASH_SNIPPET"
# Add `source $RESOURCES_DIR/auto_venv.sh` to your ~/.bashrc to activate
# Add `source "$VENV_BASH_SNIPPET"` to your ~/.bashrc to activate
# the Pimoroni virtual environment automagically!
VENV_DIR="$VENV_DIR"
if [ ! -f \$VENV_DIR/bin/activate ]; then
Expand Down
Loading