mirror of
https://github.com/pimoroni/grow-python
synced 2025-10-25 15:19:23 +00:00
Port to gpiod/gpiodevice.
This commit is contained in:
@@ -4,18 +4,28 @@ import atexit
|
|||||||
import threading
|
import threading
|
||||||
import time
|
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():
|
class Piezo():
|
||||||
def __init__(self, gpio_pin=13):
|
def __init__(self, gpio_pin=None):
|
||||||
GPIO.setmode(GPIO.BCM)
|
|
||||||
GPIO.setwarnings(False)
|
if gpio_pin is None:
|
||||||
GPIO.setup(gpio_pin, GPIO.OUT, initial=GPIO.LOW)
|
gpio_pin = gpiodevice.get_pins_for_platform(PLATFORMS)[0]
|
||||||
self.pwm = GPIO.PWM(gpio_pin, 440)
|
elif isinstance(gpio_pin, str):
|
||||||
self.pwm.start(0)
|
gpio_pin = gpiodevice.get_pin(gpio_pin, "piezo", pwm.OUTL)
|
||||||
|
|
||||||
|
self.pwm = pwm.PWM(gpio_pin)
|
||||||
self._timeout = None
|
self._timeout = None
|
||||||
atexit.register(self._exit)
|
pwm.PWM.start_thread()
|
||||||
|
atexit.register(pwm.PWM.stop_thread)
|
||||||
|
|
||||||
def frequency(self, value):
|
def frequency(self, value):
|
||||||
"""Change the piezo frequency.
|
"""Change the piezo frequency.
|
||||||
@@ -23,17 +33,15 @@ class Piezo():
|
|||||||
Loosely corresponds to musical pitch, if you suspend disbelief.
|
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.
|
"""Start the piezo.
|
||||||
|
|
||||||
Sets the Duty Cycle to 100%
|
Sets the Duty Cycle to 50%
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if frequency is not None:
|
self.pwm.start(frequency=frequency, duty_cycle=0.5)
|
||||||
self.frequency(frequency)
|
|
||||||
self.pwm.ChangeDutyCycle(1)
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Stop the piezo.
|
"""Stop the piezo.
|
||||||
@@ -41,7 +49,7 @@ class Piezo():
|
|||||||
Sets the Duty Cycle to 0%
|
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):
|
def beep(self, frequency=440, timeout=0.1, blocking=True, force=False):
|
||||||
"""Beep the piezo for time seconds.
|
"""Beep the piezo for time seconds.
|
||||||
@@ -67,6 +75,3 @@ class Piezo():
|
|||||||
self.start(frequency=frequency)
|
self.start(frequency=frequency)
|
||||||
self._timeout.start()
|
self._timeout.start()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _exit(self):
|
|
||||||
self.pwm.stop()
|
|
||||||
|
|||||||
40
grow/pump.py
40
grow/pump.py
@@ -2,13 +2,20 @@ import atexit
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import RPi.GPIO as GPIO
|
import gpiodevice
|
||||||
|
|
||||||
PUMP_1_PIN = 17
|
from . import pwm
|
||||||
PUMP_2_PIN = 27
|
|
||||||
PUMP_3_PIN = 22
|
PUMP_1_PIN = "PIN11" # 17
|
||||||
|
PUMP_2_PIN = "PIN13" # 27
|
||||||
|
PUMP_3_PIN = "PIN15" # 22
|
||||||
PUMP_PWM_FREQ = 10000
|
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()
|
global_lock = threading.Lock()
|
||||||
@@ -17,6 +24,8 @@ global_lock = threading.Lock()
|
|||||||
class Pump(object):
|
class Pump(object):
|
||||||
"""Grow pump driver."""
|
"""Grow pump driver."""
|
||||||
|
|
||||||
|
PINS = None
|
||||||
|
|
||||||
def __init__(self, channel=1):
|
def __init__(self, channel=1):
|
||||||
"""Create a new pump.
|
"""Create a new pump.
|
||||||
|
|
||||||
@@ -26,22 +35,19 @@ class Pump(object):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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)
|
self._gpio_pin = Pump.PINS[channel - 1]
|
||||||
GPIO.setwarnings(False)
|
|
||||||
GPIO.setup(self._gpio_pin, GPIO.OUT, initial=GPIO.LOW)
|
self._pwm = pwm.PWM(self._gpio_pin, PUMP_PWM_FREQ)
|
||||||
self._pwm = GPIO.PWM(self._gpio_pin, PUMP_PWM_FREQ)
|
|
||||||
self._pwm.start(0)
|
self._pwm.start(0)
|
||||||
|
|
||||||
|
pwm.PWM.start_thread()
|
||||||
|
atexit.register(pwm.PWM.stop_thread)
|
||||||
|
|
||||||
self._timeout = None
|
self._timeout = None
|
||||||
|
|
||||||
atexit.register(self._stop)
|
|
||||||
|
|
||||||
def _stop(self):
|
|
||||||
self._pwm.stop(0)
|
|
||||||
GPIO.setup(self._gpio_pin, GPIO.IN)
|
|
||||||
|
|
||||||
def set_speed(self, speed):
|
def set_speed(self, speed):
|
||||||
"""Set pump speed (PWM duty cycle)."""
|
"""Set pump speed (PWM duty cycle)."""
|
||||||
if speed > 1.0 or speed < 0:
|
if speed > 1.0 or speed < 0:
|
||||||
@@ -52,7 +58,7 @@ class Pump(object):
|
|||||||
elif not global_lock.acquire(blocking=False):
|
elif not global_lock.acquire(blocking=False):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._pwm.ChangeDutyCycle(int(PUMP_MAX_DUTY * speed))
|
self._pwm.set_duty_cycle(PUMP_MAX_DUTY * speed)
|
||||||
self._speed = speed
|
self._speed = speed
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
106
grow/pwm.py
Normal file
106
grow/pwm.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import time
|
||||||
|
from threading import 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
|
||||||
|
_pwm_running: bool = False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def start_thread():
|
||||||
|
if PWM._t_pwm is None:
|
||||||
|
PWM._pwm_running = True
|
||||||
|
PWM._t_pwm = Thread(target=PWM._run)
|
||||||
|
PWM._t_pwm.start()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def stop_thread():
|
||||||
|
if PWM._t_pwm is not None:
|
||||||
|
PWM._pwm_running = False
|
||||||
|
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)
|
||||||
|
del PWM._pwms[index]
|
||||||
|
if len(PWM._pwms) == 0:
|
||||||
|
PWM.stop_thread()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _run():
|
||||||
|
while PWM._pwm_running:
|
||||||
|
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)
|
||||||
@@ -36,8 +36,10 @@ classifiers = [
|
|||||||
"Topic :: System :: Hardware",
|
"Topic :: System :: Hardware",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ltr559",
|
"gpiodevice",
|
||||||
"st7735>=0.0.5",
|
"gpiod>=2.1.3",
|
||||||
|
"ltr559>=1.0.0",
|
||||||
|
"st7735>=1.0.0",
|
||||||
"pyyaml",
|
"pyyaml",
|
||||||
"fonts",
|
"fonts",
|
||||||
"font-roboto"
|
"font-roboto"
|
||||||
@@ -121,5 +123,11 @@ ignore = [
|
|||||||
|
|
||||||
[tool.pimoroni]
|
[tool.pimoroni]
|
||||||
apt_packages = []
|
apt_packages = []
|
||||||
configtxt = []
|
configtxt = [
|
||||||
commands = []
|
"dtoverlay=spi0-cs,cs0_pin=14" # Re-assign CS0 from BCM 8 so that Grow can use it
|
||||||
|
]
|
||||||
|
commands = [
|
||||||
|
"printf \"Setting up i2c and SPI..\\n\"",
|
||||||
|
"sudo raspi-config nonint do_spi 0",
|
||||||
|
"sudo raspi-config nonint do_i2c 0"
|
||||||
|
]
|
||||||
|
|||||||
@@ -18,31 +18,20 @@ class SMBusFakeDevice(MockSMBus):
|
|||||||
@pytest.fixture(scope='function', autouse=True)
|
@pytest.fixture(scope='function', autouse=True)
|
||||||
def cleanup():
|
def cleanup():
|
||||||
yield None
|
yield None
|
||||||
try:
|
for module in ['grow', 'grow.moisture', 'grow.pump']:
|
||||||
del sys.modules['grow']
|
try:
|
||||||
except KeyError:
|
del sys.modules[module]
|
||||||
pass
|
except KeyError:
|
||||||
try:
|
continue
|
||||||
del sys.modules['grow.moisture']
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
del sys.modules['grow.pump']
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function', autouse=False)
|
@pytest.fixture(scope='function', autouse=False)
|
||||||
def GPIO():
|
def GPIO():
|
||||||
"""Mock RPi.GPIO module."""
|
"""Mock gpiod module."""
|
||||||
GPIO = mock.MagicMock()
|
gpiod = mock.MagicMock()
|
||||||
# Fudge for Python < 37 (possibly earlier)
|
sys.modules['gpiod'] = gpiod
|
||||||
sys.modules['RPi'] = mock.Mock()
|
yield gpiod
|
||||||
sys.modules['RPi'].GPIO = GPIO
|
del sys.modules['gpiod']
|
||||||
sys.modules['RPi.GPIO'] = GPIO
|
|
||||||
yield GPIO
|
|
||||||
del sys.modules['RPi']
|
|
||||||
del sys.modules['RPi.GPIO']
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function', autouse=False)
|
@pytest.fixture(scope='function', autouse=False)
|
||||||
@@ -55,13 +44,13 @@ def spidev():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function', autouse=False)
|
@pytest.fixture(scope='function', autouse=False)
|
||||||
def smbus():
|
def smbus2():
|
||||||
"""Mock smbus module."""
|
"""Mock smbus2 module."""
|
||||||
smbus = mock.MagicMock()
|
smbus2 = mock.MagicMock()
|
||||||
smbus.SMBus = SMBusFakeDevice
|
smbus2.SMBus = SMBusFakeDevice
|
||||||
sys.modules['smbus'] = smbus
|
sys.modules['smbus2'] = smbus2
|
||||||
yield smbus
|
yield smbus2
|
||||||
del sys.modules['smbus']
|
del sys.modules['smbus2']
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function', autouse=False)
|
@pytest.fixture(scope='function', autouse=False)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
def test_pumps_actually_stop(GPIO, smbus):
|
def test_pumps_actually_stop(gpiod, smbus2):
|
||||||
from grow.pump import Pump
|
from grow.pump import Pump
|
||||||
|
|
||||||
ch1 = Pump(channel=1)
|
ch1 = Pump(channel=1)
|
||||||
@@ -11,7 +11,7 @@ def test_pumps_actually_stop(GPIO, smbus):
|
|||||||
assert ch1.get_speed() == 0
|
assert ch1.get_speed() == 0
|
||||||
|
|
||||||
|
|
||||||
def test_pumps_are_mutually_exclusive(GPIO, smbus):
|
def test_pumps_are_mutually_exclusive(gpiod, smbus2):
|
||||||
from grow.pump import Pump, global_lock
|
from grow.pump import Pump, global_lock
|
||||||
|
|
||||||
ch1 = Pump(channel=1)
|
ch1 = Pump(channel=1)
|
||||||
@@ -29,7 +29,7 @@ def test_pumps_are_mutually_exclusive(GPIO, smbus):
|
|||||||
assert ch3.dose(speed=0.5, blocking=False) is False
|
assert ch3.dose(speed=0.5, blocking=False) is False
|
||||||
|
|
||||||
|
|
||||||
def test_pumps_run_sequentially(GPIO, smbus):
|
def test_pumps_run_sequentially(gpiod, smbus2):
|
||||||
from grow.pump import Pump, global_lock
|
from grow.pump import Pump, global_lock
|
||||||
|
|
||||||
ch1 = Pump(channel=1)
|
ch1 = Pump(channel=1)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import mock
|
import mock
|
||||||
|
|
||||||
|
|
||||||
def test_moisture_setup(GPIO, smbus):
|
def test_moisture_setup(gpiod, smbus2):
|
||||||
from grow.moisture import Moisture
|
from grow.moisture import Moisture
|
||||||
|
|
||||||
ch1 = Moisture(channel=1)
|
ch1 = Moisture(channel=1)
|
||||||
@@ -15,7 +15,7 @@ def test_moisture_setup(GPIO, smbus):
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
def test_moisture_read(GPIO, smbus):
|
def test_moisture_read(gpiod, smbus2):
|
||||||
from grow.moisture import Moisture
|
from grow.moisture import Moisture
|
||||||
|
|
||||||
assert Moisture(channel=1).saturation == 1.0
|
assert Moisture(channel=1).saturation == 1.0
|
||||||
@@ -27,7 +27,7 @@ def test_moisture_read(GPIO, smbus):
|
|||||||
assert Moisture(channel=3).moisture == 0
|
assert Moisture(channel=3).moisture == 0
|
||||||
|
|
||||||
|
|
||||||
def test_pump_setup(GPIO, smbus):
|
def test_pump_setup(gpiod, smbus2):
|
||||||
from grow.pump import PUMP_PWM_FREQ, Pump
|
from grow.pump import PUMP_PWM_FREQ, Pump
|
||||||
|
|
||||||
ch1 = Pump(channel=1)
|
ch1 = Pump(channel=1)
|
||||||
|
|||||||
Reference in New Issue
Block a user