Skip to content

Commit bc22e3f

Browse files
committed
Port to gpiod/gpiodevice.
1 parent 7513fde commit bc22e3f

File tree

7 files changed

+187
-73
lines changed

7 files changed

+187
-73
lines changed

grow/__init__.py

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,52 @@
44
import threading
55
import time
66

7-
import RPi.GPIO as GPIO
7+
import gpiodevice
8+
9+
from . import pwm
10+
11+
PLATFORMS = {
12+
"Raspberry Pi 5": {"piezo": ("PIN33", pwm.OUTL)},
13+
"Raspberry Pi 4": {"piezo": ("GPIO13", pwm.OUTL)},
14+
}
815

916

1017
class Piezo():
11-
def __init__(self, gpio_pin=13):
12-
GPIO.setmode(GPIO.BCM)
13-
GPIO.setwarnings(False)
14-
GPIO.setup(gpio_pin, GPIO.OUT, initial=GPIO.LOW)
15-
self.pwm = GPIO.PWM(gpio_pin, 440)
16-
self.pwm.start(0)
18+
def __init__(self, gpio_pin=None):
19+
20+
if gpio_pin is None:
21+
gpio_pin = gpiodevice.get_pins_for_platform(PLATFORMS)[0]
22+
elif isinstance(gpio_pin, str):
23+
gpio_pin = gpiodevice.get_pin(gpio_pin, "piezo", pwm.OUTL)
24+
25+
self.pwm = pwm.PWM(gpio_pin)
1726
self._timeout = None
18-
atexit.register(self._exit)
27+
pwm.PWM.start_thread()
28+
atexit.register(pwm.PWM.stop_thread)
1929

2030
def frequency(self, value):
2131
"""Change the piezo frequency.
2232
2333
Loosely corresponds to musical pitch, if you suspend disbelief.
2434
2535
"""
26-
self.pwm.ChangeFrequency(value)
36+
self.pwm.set_frequency(value)
2737

28-
def start(self, frequency=None):
38+
def start(self, frequency):
2939
"""Start the piezo.
3040
31-
Sets the Duty Cycle to 100%
41+
Sets the Duty Cycle to 50%
3242
3343
"""
34-
if frequency is not None:
35-
self.frequency(frequency)
36-
self.pwm.ChangeDutyCycle(1)
44+
self.pwm.start(frequency=frequency, duty_cycle=0.5)
3745

3846
def stop(self):
3947
"""Stop the piezo.
4048
4149
Sets the Duty Cycle to 0%
4250
4351
"""
44-
self.pwm.ChangeDutyCycle(0)
52+
self.pwm.stop()
4553

4654
def beep(self, frequency=440, timeout=0.1, blocking=True, force=False):
4755
"""Beep the piezo for time seconds.
@@ -67,6 +75,3 @@ def beep(self, frequency=440, timeout=0.1, blocking=True, force=False):
6775
self.start(frequency=frequency)
6876
self._timeout.start()
6977
return True
70-
71-
def _exit(self):
72-
self.pwm.stop()

grow/pump.py

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@
22
import threading
33
import time
44

5-
import RPi.GPIO as GPIO
5+
import gpiodevice
66

7-
PUMP_1_PIN = 17
8-
PUMP_2_PIN = 27
9-
PUMP_3_PIN = 22
7+
from . import pwm
8+
9+
PUMP_1_PIN = "PIN11" # 17
10+
PUMP_2_PIN = "PIN13" # 27
11+
PUMP_3_PIN = "PIN15" # 22
1012
PUMP_PWM_FREQ = 10000
11-
PUMP_MAX_DUTY = 90
13+
PUMP_MAX_DUTY = 0.9
14+
15+
PLATFORMS = {
16+
"Raspberry Pi 5": {"pump1": ("PIN11", pwm.OUTL), "pump2": ("PIN12", pwm.OUTL), "pump3": ("PIN15", pwm.OUTL)},
17+
"Raspberry Pi 4": {"pump1": ("GPIO17", pwm.OUTL), "pump2": ("GPIO27", pwm.OUTL), "pump3": ("GPIO22", pwm.OUTL)},
18+
}
1219

1320

1421
global_lock = threading.Lock()
@@ -17,6 +24,8 @@
1724
class Pump(object):
1825
"""Grow pump driver."""
1926

27+
PINS = None
28+
2029
def __init__(self, channel=1):
2130
"""Create a new pump.
2231
@@ -26,21 +35,18 @@ def __init__(self, channel=1):
2635
2736
"""
2837

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

31-
GPIO.setmode(GPIO.BCM)
32-
GPIO.setwarnings(False)
33-
GPIO.setup(self._gpio_pin, GPIO.OUT, initial=GPIO.LOW)
34-
self._pwm = GPIO.PWM(self._gpio_pin, PUMP_PWM_FREQ)
35-
self._pwm.start(0)
41+
self._gpio_pin = Pump.PINS[channel - 1]
3642

37-
self._timeout = None
43+
self._pwm = pwm.PWM(self._gpio_pin, PUMP_PWM_FREQ)
44+
self._pwm.start(0)
3845

39-
atexit.register(self._stop)
46+
pwm.PWM.start_thread()
47+
atexit.register(pwm.PWM.stop_thread)
4048

41-
def _stop(self):
42-
self._pwm.stop(0)
43-
GPIO.setup(self._gpio_pin, GPIO.IN)
49+
self._timeout = None
4450

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

55-
self._pwm.ChangeDutyCycle(int(PUMP_MAX_DUTY * speed))
61+
self._pwm.set_duty_cycle(PUMP_MAX_DUTY * speed)
5662
self._speed = speed
5763
return True
5864

grow/pwm.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import time
2+
from threading import Thread
3+
4+
import gpiod
5+
import gpiodevice
6+
from gpiod.line import Direction, Value
7+
8+
OUTL = gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE)
9+
10+
11+
class PWM:
12+
_pwms: list = []
13+
_t_pwm: Thread = None
14+
_pwm_running: bool = False
15+
16+
@staticmethod
17+
def start_thread():
18+
if PWM._t_pwm is None:
19+
PWM._pwm_running = True
20+
PWM._t_pwm = Thread(target=PWM._run)
21+
PWM._t_pwm.start()
22+
23+
@staticmethod
24+
def stop_thread():
25+
if PWM._t_pwm is not None:
26+
PWM._pwm_running = False
27+
PWM._t_pwm.join()
28+
PWM._t_pwm = None
29+
30+
@staticmethod
31+
def _add(pwm):
32+
PWM._pwms.append(pwm)
33+
34+
@staticmethod
35+
def _remove(pwm):
36+
index = PWM._pwms.index(pwm)
37+
del PWM._pwms[index]
38+
if len(PWM._pwms) == 0:
39+
PWM.stop_thread()
40+
41+
@staticmethod
42+
def _run():
43+
while PWM._pwm_running:
44+
PWM.run()
45+
46+
@staticmethod
47+
def run():
48+
for pwm in PWM._pwms:
49+
pwm.next(time.time())
50+
51+
def __init__(self, pin, frequency=0, duty_cycle=0, lines=None, offset=None):
52+
self.duty_cycle = 0
53+
self.frequency = 0
54+
self.duty_period = 0
55+
self.period = 0
56+
self.running = False
57+
self.time_start = None
58+
self.state = Value.ACTIVE
59+
60+
self.set_frequency(frequency)
61+
self.set_duty_cycle(duty_cycle)
62+
63+
if isinstance(pin, tuple):
64+
self.lines, self.offset = pin
65+
else:
66+
self.lines, self.offset = gpiodevice.get_pin(pin, "PWM", OUTL)
67+
68+
PWM._add(self)
69+
70+
def set_frequency(self, frequency):
71+
if frequency == 0:
72+
return
73+
self.frequency = frequency
74+
self.period = 1.0 / frequency
75+
self.duty_period = self.duty_cycle * self.period
76+
77+
def set_duty_cycle(self, duty_cycle):
78+
self.duty_cycle = duty_cycle
79+
self.duty_period = self.duty_cycle * self.period
80+
81+
def start(self, duty_cycle=None, frequency=None, start_time=None):
82+
if duty_cycle is not None:
83+
self.set_duty_cycle(duty_cycle)
84+
85+
if frequency is not None:
86+
self.set_frequency(frequency)
87+
88+
self.time_start = time.time() if start_time is None else start_time
89+
90+
self.running = True
91+
92+
def next(self, t):
93+
if not self.running:
94+
return
95+
d = t - self.time_start
96+
d %= self.period
97+
new_state = Value.ACTIVE if d < self.duty_period else Value.INACTIVE
98+
if new_state != self.state:
99+
self.lines.set_value(self.offset, new_state)
100+
self.state = new_state
101+
102+
def stop(self):
103+
self.running = False
104+
105+
def __del__(self):
106+
PWM._remove(self)

pyproject.toml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ classifiers = [
3636
"Topic :: System :: Hardware",
3737
]
3838
dependencies = [
39-
"ltr559",
40-
"st7735>=0.0.5",
39+
"gpiodevice",
40+
"gpiod>=2.1.3",
41+
"ltr559>=1.0.0",
42+
"st7735>=1.0.0",
4143
"pyyaml",
4244
"fonts",
4345
"font-roboto"
@@ -121,5 +123,11 @@ ignore = [
121123

122124
[tool.pimoroni]
123125
apt_packages = []
124-
configtxt = []
125-
commands = []
126+
configtxt = [
127+
"dtoverlay=spi0-cs,cs0_pin=14" # Re-assign CS0 from BCM 8 so that Grow can use it
128+
]
129+
commands = [
130+
"printf \"Setting up i2c and SPI..\\n\"",
131+
"sudo raspi-config nonint do_spi 0",
132+
"sudo raspi-config nonint do_i2c 0"
133+
]

tests/conftest.py

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,31 +18,20 @@ def __init__(self, i2c_bus):
1818
@pytest.fixture(scope='function', autouse=True)
1919
def cleanup():
2020
yield None
21-
try:
22-
del sys.modules['grow']
23-
except KeyError:
24-
pass
25-
try:
26-
del sys.modules['grow.moisture']
27-
except KeyError:
28-
pass
29-
try:
30-
del sys.modules['grow.pump']
31-
except KeyError:
32-
pass
21+
for module in ['grow', 'grow.moisture', 'grow.pump']:
22+
try:
23+
del sys.modules[module]
24+
except KeyError:
25+
continue
3326

3427

3528
@pytest.fixture(scope='function', autouse=False)
3629
def GPIO():
37-
"""Mock RPi.GPIO module."""
38-
GPIO = mock.MagicMock()
39-
# Fudge for Python < 37 (possibly earlier)
40-
sys.modules['RPi'] = mock.Mock()
41-
sys.modules['RPi'].GPIO = GPIO
42-
sys.modules['RPi.GPIO'] = GPIO
43-
yield GPIO
44-
del sys.modules['RPi']
45-
del sys.modules['RPi.GPIO']
30+
"""Mock gpiod module."""
31+
gpiod = mock.MagicMock()
32+
sys.modules['gpiod'] = gpiod
33+
yield gpiod
34+
del sys.modules['gpiod']
4635

4736

4837
@pytest.fixture(scope='function', autouse=False)
@@ -55,13 +44,13 @@ def spidev():
5544

5645

5746
@pytest.fixture(scope='function', autouse=False)
58-
def smbus():
59-
"""Mock smbus module."""
60-
smbus = mock.MagicMock()
61-
smbus.SMBus = SMBusFakeDevice
62-
sys.modules['smbus'] = smbus
63-
yield smbus
64-
del sys.modules['smbus']
47+
def smbus2():
48+
"""Mock smbus2 module."""
49+
smbus2 = mock.MagicMock()
50+
smbus2.SMBus = SMBusFakeDevice
51+
sys.modules['smbus2'] = smbus2
52+
yield smbus2
53+
del sys.modules['smbus2']
6554

6655

6756
@pytest.fixture(scope='function', autouse=False)

tests/test_lock.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import time
22

33

4-
def test_pumps_actually_stop(GPIO, smbus):
4+
def test_pumps_actually_stop(gpiod, smbus2):
55
from grow.pump import Pump
66

77
ch1 = Pump(channel=1)
@@ -11,7 +11,7 @@ def test_pumps_actually_stop(GPIO, smbus):
1111
assert ch1.get_speed() == 0
1212

1313

14-
def test_pumps_are_mutually_exclusive(GPIO, smbus):
14+
def test_pumps_are_mutually_exclusive(gpiod, smbus2):
1515
from grow.pump import Pump, global_lock
1616

1717
ch1 = Pump(channel=1)
@@ -29,7 +29,7 @@ def test_pumps_are_mutually_exclusive(GPIO, smbus):
2929
assert ch3.dose(speed=0.5, blocking=False) is False
3030

3131

32-
def test_pumps_run_sequentially(GPIO, smbus):
32+
def test_pumps_run_sequentially(gpiod, smbus2):
3333
from grow.pump import Pump, global_lock
3434

3535
ch1 = Pump(channel=1)

tests/test_setup.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import mock
22

33

4-
def test_moisture_setup(GPIO, smbus):
4+
def test_moisture_setup(gpiod, smbus2):
55
from grow.moisture import Moisture
66

77
ch1 = Moisture(channel=1)
@@ -15,7 +15,7 @@ def test_moisture_setup(GPIO, smbus):
1515
])
1616

1717

18-
def test_moisture_read(GPIO, smbus):
18+
def test_moisture_read(gpiod, smbus2):
1919
from grow.moisture import Moisture
2020

2121
assert Moisture(channel=1).saturation == 1.0
@@ -27,7 +27,7 @@ def test_moisture_read(GPIO, smbus):
2727
assert Moisture(channel=3).moisture == 0
2828

2929

30-
def test_pump_setup(GPIO, smbus):
30+
def test_pump_setup(gpiod, smbus2):
3131
from grow.pump import PUMP_PWM_FREQ, Pump
3232

3333
ch1 = Pump(channel=1)

0 commit comments

Comments
 (0)