Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit eb32bdf

Browse files
committed
rework raspberry pi driver with digitalio
1 parent 173b6e5 commit eb32bdf

10 files changed

Lines changed: 929 additions & 670 deletions

File tree

.devfile/Containerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uvx /bin/uvx
1313

1414
USER root
1515

16-
RUN dnf -y install make git python3.12 libusbx python3-pyusb golang podman && dnf clean all
16+
RUN dnf -y install make git python3.12 python3.12-devel libusbx python3-pyusb golang podman gcc && dnf clean all
1717

1818
USER 10001
1919

.devfile/Containerfile.client

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ USER root
2424
# switch to python 3.12 as the default
2525
RUN rm -rf /usr/bin/python && ln -s /usr/bin/python3.12 /usr/bin/python
2626

27-
RUN dnf -y install make git python3.12 python3.12 libusbx python3-pyusb python3.12-pip golang && dnf clean all
27+
RUN dnf -y install make git python3.12 python3.12-devel libusbx python3-pyusb python3.12-pip golang gcc && dnf clean all
2828

2929
USER 10001
3030

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ RUN dnf install -y make git && \
77
COPY --from=uv /uv /uvx /bin/
88

99
FROM fedora:40 AS product
10-
RUN dnf install -y python3 ustreamer libusb1 && \
10+
RUN dnf install -y python3 python3-devel ustreamer libusb1 gcc && \
1111
dnf clean all && \
1212
rm -rf /var/cache/dnf
1313
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

docs/source/api-reference/drivers/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ can.md
1111
pyserial.md
1212
sdwire.md
1313
ustreamer.md
14+
raspberrypi.md
1415
```
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Raspberry Pi drivers
2+
3+
Raspberry Pi drivers are a set of drivers for the various peripherals on Pi and similar single board computers.
4+
5+
## Driver configuration
6+
```yaml
7+
export:
8+
my_serial:
9+
type: "jumpstarter_driver_raspberrypi.driver.DigitalIO"
10+
config:
11+
pin: "D3"
12+
```
13+
14+
### Config parameters
15+
16+
| Parameter | Description | Type | Required | Default |
17+
|-----------|-------------|------|----------|---------|
18+
| pin | Name of the GPIO pin to connect to, in [Adafruit Blinka format](https://docs.circuitpython.org/projects/blinka/en/latest/index.html#usage-example) | str | yes | |
19+
20+
## DigitalIOClient API
21+
```{eval-rst}
22+
.. autoclass:: jumpstarter_driver_raspberrypi.client.DigitalIOClient
23+
:members:
24+
```
25+
26+
## Examples
27+
Switch pin to push pull output and set output to high
28+
```{testcode}
29+
digitalioclient.switch_to_output(value=False, drive_mode=digitalio.DriveMode.PUSH_PULL) # default to low
30+
digitalioclient.value = True
31+
```
32+
33+
Switch pin to input with pull up and read value
34+
```{testcode}
35+
digitalioclient.switch_to_input(pull=digitalio.Pull.UP)
36+
print(digitalioclient.value)
37+
```
Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,54 @@
11
from dataclasses import dataclass
22

3+
from digitalio import DriveMode, Pull
34
from jumpstarter.client import DriverClient
45

56

67
@dataclass(kw_only=True)
7-
class DigitalOutputClient(DriverClient):
8-
def off(self):
9-
self.call("off")
8+
class DigitalIOClient(DriverClient):
9+
"""DigitalIO (Digital GPIO) client class
1010
11-
def on(self):
12-
self.call("on")
11+
Client methods for the DigitalIO driver.
12+
"""
1313

14+
def switch_to_output(self, value: bool = False, drive_mode: DriveMode = DriveMode.PUSH_PULL) -> None:
15+
"""
16+
Switch pin to output mode with given default value and drive mode
17+
"""
1418

15-
@dataclass(kw_only=True)
16-
class DigitalInputClient(DriverClient):
17-
def wait_for_active(self, timeout: float | None = None):
18-
self.call("wait_for_active", timeout)
19+
match drive_mode:
20+
case DriveMode.PUSH_PULL:
21+
drive_mode = 0
22+
case DriveMode.OPEN_DRAIN:
23+
drive_mode = 1
24+
case _:
25+
raise ValueError("unrecognized drive_mode")
26+
self.call("switch_to_output", value, drive_mode)
27+
28+
def switch_to_input(self, pull: Pull | None = None) -> None:
29+
"""
30+
Switch pin to input mode with given pull up/down mode
31+
"""
32+
33+
match pull:
34+
case None:
35+
pull = 0
36+
case Pull.UP:
37+
pull = 1
38+
case Pull.DOWN:
39+
pull = 2
40+
case _:
41+
raise ValueError("unrecognized pull")
42+
self.call("switch_to_input", pull)
43+
44+
@property
45+
def value(self) -> bool:
46+
"""
47+
Current value of the pin
48+
"""
49+
50+
return self.call("get_value")
1951

20-
def wait_for_inactive(self, timeout: float | None = None):
21-
self.call("wait_for_inactive", timeout)
52+
@value.setter
53+
def value(self, value: bool) -> None:
54+
self.call("set_value", value)
Lines changed: 94 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,124 @@
1+
from collections.abc import AsyncGenerator
12
from dataclasses import dataclass, field
3+
from time import sleep
24

3-
from gpiozero import DigitalInputDevice, DigitalOutputDevice, InputDevice
5+
import board
6+
from digitalio import DigitalInOut, DriveMode, Pull
47
from jumpstarter.driver import Driver, export
8+
from jumpstarter_driver_power.driver import PowerInterface, PowerReading
59

610

711
@dataclass(kw_only=True)
8-
class DigitalOutput(Driver):
9-
pin: int | str
10-
device: InputDevice = field(init=False) # Start as input
12+
class DigitalIO(Driver):
13+
pin: str
14+
device: DigitalInOut = field(init=False)
1115

1216
@classmethod
1317
def client(cls) -> str:
14-
return "jumpstarter_driver_raspberrypi.client.DigitalOutputClient"
18+
return "jumpstarter_driver_raspberrypi.client.DigitalIOClient"
1519

1620
def __post_init__(self):
1721
super().__post_init__()
18-
# Initialize as InputDevice first
19-
self.device = InputDevice(pin=self.pin)
22+
# Defaults to input with no pull
23+
try:
24+
self.device = DigitalInOut(pin=getattr(board, self.pin))
25+
except AttributeError as err:
26+
raise ValueError(f"Invalid pin name: {self.pin}") from err
2027

2128
def close(self):
2229
if hasattr(self, "device"):
23-
self.device.close()
24-
super().close()
30+
self.device.deinit()
2531

2632
@export
27-
def off(self):
28-
if not isinstance(self.device, DigitalOutputDevice):
29-
self.device.close()
30-
self.device = DigitalOutputDevice(pin=self.pin, initial_value=None)
31-
self.device.off()
33+
def switch_to_output(self, value: bool = False, drive_mode: int = 0) -> None:
34+
match drive_mode:
35+
case 0:
36+
drive_mode = DriveMode.PUSH_PULL
37+
case 1:
38+
drive_mode = DriveMode.OPEN_DRAIN
39+
case _:
40+
raise ValueError("unrecognized drive_mode")
41+
42+
self.device.switch_to_output(value, drive_mode)
43+
44+
@export
45+
def switch_to_input(self, pull: int = 0) -> None:
46+
match pull:
47+
case 0:
48+
pull = None
49+
case 1:
50+
pull = Pull.UP
51+
case 2:
52+
pull = Pull.DOWN
53+
case _:
54+
raise ValueError("unrecognized pull")
55+
56+
self.device.switch_to_input(pull)
57+
58+
@export
59+
def set_value(self, value: bool) -> None:
60+
self.device.value = value
3261

3362
@export
34-
def on(self):
35-
if not isinstance(self.device, DigitalOutputDevice):
36-
self.device.close()
37-
self.device = DigitalOutputDevice(pin=self.pin, initial_value=None)
38-
self.device.on()
63+
def get_value(self) -> bool:
64+
return self.device.value
3965

4066

4167
@dataclass(kw_only=True)
42-
class DigitalInput(Driver):
43-
pin: int | str
44-
device: DigitalInputDevice = field(init=False)
68+
class DigitalPowerSwitch(PowerInterface, DigitalIO):
69+
value: bool = False
70+
drive_mode: str = "PUSH_PULL"
71+
72+
def __post_init__(self):
73+
super().__post_init__()
74+
75+
try:
76+
self.device.switch_to_output(value=self.value, drive_mode=getattr(DriveMode, self.drive_mode))
77+
except AttributeError as err:
78+
raise ValueError(f"Invalid drive mode: {self.drive_mode}") from err
79+
80+
@export
81+
def on(self) -> None:
82+
self.device.value = True
83+
84+
@export
85+
def off(self) -> None:
86+
self.device.value = False
87+
88+
@export
89+
def read(self) -> AsyncGenerator[PowerReading, None]:
90+
raise NotImplementedError
4591

46-
@classmethod
47-
def client(cls) -> str:
48-
return "jumpstarter_driver_raspberrypi.client.DigitalInputClient"
92+
93+
@dataclass(kw_only=True)
94+
class DigitalPowerButton(PowerInterface, DigitalIO):
95+
value: bool = False
96+
drive_mode: str = "OPEN_DRAIN"
97+
on_press_seconds: int = 1
98+
off_press_seconds: int = 5
4999

50100
def __post_init__(self):
51101
super().__post_init__()
52-
self.device = DigitalInputDevice(pin=self.pin)
102+
103+
try:
104+
self.device.switch_to_output(value=self.value, drive_mode=getattr(DriveMode, self.drive_mode))
105+
except AttributeError as err:
106+
raise ValueError(f"Invalid drive mode: {self.drive_mode}") from err
107+
108+
def press(self, seconds: int) -> None:
109+
self.device.value = self.value
110+
self.device.value = not self.value
111+
sleep(seconds)
112+
self.device.value = self.value
113+
114+
@export
115+
def on(self) -> None:
116+
self.press(self.on_press_seconds)
53117

54118
@export
55-
def wait_for_active(self, timeout: float | None = None):
56-
self.device.wait_for_active(timeout)
119+
def off(self) -> None:
120+
self.press(self.off_press_seconds)
57121

58122
@export
59-
def wait_for_inactive(self, timeout: float | None = None):
60-
self.device.wait_for_inactive(timeout)
123+
def read(self) -> AsyncGenerator[PowerReading, None]:
124+
raise NotImplementedError
Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,45 @@
1-
from concurrent.futures import ThreadPoolExecutor
2-
3-
from gpiozero import Device
4-
from gpiozero.pins.mock import MockFactory
51
from jumpstarter.common.utils import serve
62

7-
from jumpstarter_driver_raspberrypi.driver import DigitalInput, DigitalOutput
83

9-
Device.pin_factory = MockFactory()
4+
def test_drivers_gpio_digital_input(monkeypatch):
5+
monkeypatch.setenv("BLINKA_OS_AGNOSTIC", "1")
106

7+
from digitalio import Pull
118

12-
def test_drivers_gpio_digital_output():
13-
pin_factory = MockFactory()
14-
Device.pin_factory = pin_factory
15-
pin_number = 1
16-
mock_pin = pin_factory.pin(pin_number)
9+
from jumpstarter_driver_raspberrypi.driver import DigitalIO
1710

18-
instance = DigitalOutput(pin=pin_number)
11+
with serve(DigitalIO(pin="Dx_INPUT_TOGGLE")) as client:
12+
client.switch_to_input(pull=Pull.UP)
13+
assert client.value
14+
assert not client.value
15+
assert client.value
1916

20-
assert not mock_pin.state
2117

22-
with serve(instance) as client:
23-
client.off()
24-
assert not mock_pin.state
18+
def test_drivers_gpio_digital_output(monkeypatch):
19+
monkeypatch.setenv("BLINKA_OS_AGNOSTIC", "1")
2520

26-
client.on()
27-
assert mock_pin.state
21+
from digitalio import DriveMode
2822

29-
client.off()
30-
assert not mock_pin.state
23+
from jumpstarter_driver_raspberrypi.driver import DigitalIO
3124

32-
mock_pin.assert_states([False, True, False])
25+
with serve(DigitalIO(pin="Dx_OUTPUT")) as client:
26+
client.switch_to_output(value=True, drive_mode=DriveMode.PUSH_PULL)
27+
client.value = True
28+
assert client.value
29+
client.value = False
30+
# Dx_OUTPUT is always True
31+
assert client.value
3332

3433

35-
def test_drivers_gpio_digital_input():
36-
instance = DigitalInput(pin=4)
34+
def test_drivers_gpio_power(monkeypatch):
35+
monkeypatch.setenv("BLINKA_OS_AGNOSTIC", "1")
3736

38-
with serve(instance) as client:
39-
with ThreadPoolExecutor() as pool:
40-
pool.submit(client.wait_for_active)
41-
instance.device.pin.drive_high()
37+
from jumpstarter_driver_raspberrypi.driver import DigitalPowerButton, DigitalPowerSwitch
4238

43-
with ThreadPoolExecutor() as pool:
44-
pool.submit(client.wait_for_inactive)
45-
instance.device.pin.drive_low()
39+
with serve(DigitalPowerSwitch(pin="Dx_OUTPUT", drive_mode="PUSH_PULL")) as client:
40+
client.off()
41+
client.on()
42+
43+
with serve(DigitalPowerButton(pin="Dx_OUTPUT", drive_mode="PUSH_PULL", off_press_seconds=1)) as client:
44+
client.off()
45+
client.on()

packages/jumpstarter-driver-raspberrypi/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ license = { text = "Apache-2.0" }
1212
requires-python = ">=3.11"
1313
dependencies = [
1414
"jumpstarter",
15-
"gpiozero>=2.0.1",
15+
"jumpstarter-driver-power",
16+
"adafruit-blinka>=8.51.0",
1617
]
1718

1819
[dependency-groups]

0 commit comments

Comments
 (0)