TLDNR: Yes, it is possible.
I am not aware of any already prepared solution that can help you achieve what you want to do. Hence I found it very interesting whether it is feasible at all.
I was looking a while for a seam which can be used to stub the GPIO features and I have found that gpiozero uses GPIOZERO_PIN_FACTORY
environmental variable to pick a backend. The plan is to write own pin factory, that will provide possibility to test other scripts.
NOTE: Please treat my solution as a proof of concept. It is far from being production ready.
The idea is to get GPIO states out of script under test scope. My solution uses env variable RPI_STUB_URL
to get path of unix socket which will be used to communicate with the stub controller
.
I have introduced very simple one request/response per connection protocol:
- "GF {pin}\n" - ask what is the current function of the
pin
. Stub does not validate the response, but I would expect "input", "output" to be used.
- "SF {pin} {function}\n" - request change of the pin's current function. Stub does not validate the function, bu I would expect "input", "output" to be used. Stub expects "OK" as a response.
- "GS {pin}\n" - ask what is the current state of the
pin
. Stub expects values "0" or "1" as a response.
- "SS {pin} {value|]n" - request change of the pin's current state. Stub expects "OK" as a response.
My "stub package" contains following files:
- setup.py # This file is needed in every package, isn't it?
- rpi_stub/
- __init__.py # This file collects entry points
- stubPin.py # This file implements stub backend for gpiozero
- controller.py # This file implements server for my stub
- trigger.py # This file implements client side feature of my stub
Let's start with setup.py
content:
from setuptools import setup, find_packages
setup(
name="Raspberry PI GPIO stub",
version="0.1",
description="Package with stub plugin for gpiozero library",
packages=find_packages(),
install_requires = ["gpiozero"],
include_package_data=True,
entry_points="""
[console_scripts]
stub_rpi_controller=rpi_stub:controller_main
stub_rpi_trigger=rpi_stub:trigger_main
[gpiozero_pin_factories]
stub_rpi=rpi_stub:make_stub_pin
"""
)
It defines two console_scripts entry points one for controller and one for trigger. And one pin factory for gpiozero.
Now rpi_stub/__init__.py
:
import rpi_stub.stubPin
from rpi_stub.controller import controller_main
from rpi_stub.trigger import trigger_main
def make_stub_pin(number):
return stubPin.StubPin(number)
It is rather simple file.
File rpi_stub/trigger.py
:
import socket
import sys
def trigger_main():
socket_addr = sys.argv[1]
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(socket_addr)
request = "{0}\n".format(" ".join(sys.argv[2:]))
sock.sendall(request.encode())
data = sock.recv(1024)
sock.close()
print(data.decode("utf-8"))
trigger
allows you to make your own request. You can use it to check what is the state of GPIO pin or change it.
File rpi_stub/controller.py
:
import socketserver
import sys
functions = {}
states = {}
class MyHandler(socketserver.StreamRequestHandler):
def _respond(self, response):
print("Sending response: {0}".format(response))
self.wfile.write(response.encode())
def _handle_get_function(self, data):
print("Handling get_function: {0}".format(data))
try:
self._respond("{0}".format(functions[data[0]]))
except KeyError:
self._respond("input")
def _handle_set_function(self, data):
print("Handling set_function: {0}".format(data))
functions[data[0]] = data[1]
self._respond("OK")
def _handle_get_state(self, data):
print("Handling get_state: {0}".format(data))
try:
self._respond("{0}".format(states[data[0]]))
except KeyError:
self._respond("0")
def _handle_set_state(self, data):
print("Handling set_state: {0}".format(data))
states[data[0]] = data[1]
self._respond("OK")
def handle(self):
data = self.rfile.readline()
print("Handle: {0}".format(data))
data = data.decode("utf-8").strip().split(" ")
if data[0] == "GF":
self._handle_get_function(data[1:])
elif data[0] == "SF":
self._handle_set_function(data[1:])
elif data[0] == "GS":
self._handle_get_state(data[1:])
elif data[0] == "SS":
self._handle_set_state(data[1:])
else:
self._respond("Not understood")
def controller_main():
socket_addr = sys.argv[1]
server = socketserver.UnixStreamServer(socket_addr, MyHandler)
server.serve_forever()
This file contains the simplest server I was able to write.
And the most complicated file rpi_stub/stubPin.py
:
from gpiozero.pins import Pin
import os
import socket
from threading import Thread
from time import sleep
def dummy_func():
pass
def edge_detector(pin):
print("STUB: Edge detector for pin: {0} spawned".format(pin.number))
while pin._edges != "none":
new_state = pin._get_state()
print("STUB: Edge detector for pin {0}: value {1} received".format(pin.number, new_state))
if new_state != pin._last_known:
print("STUB: Edge detector for pin {0}: calling callback".format(pin.number))
pin._when_changed()
pin._last_known = new_state
sleep(1)
print("STUB: Edge detector for pin: {0} ends".format(pin.number))
class StubPin(Pin):
def __init__(self, number):
super(StubPin, self).__init__()
self.number = number
self._when_changed = dummy_func
self._edges = "none"
self._last_known = 0
def _make_request(self, request):
server_address = os.getenv("RPI_STUB_URL", None)
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(server_address)
sock.sendall(request.encode())
data = sock.recv(1024)
sock.close()
return data.decode("utf-8")
def _get_function(self):
response = self._make_request("GF {pin}\n".format(pin=self.number))
return response;
def _set_function(self, function):
response = self._make_request("SF {pin} {function}\n".format(pin=self.number, function=function))
if response != "OK":
raise Exception("STUB Not understood", response)
def _get_state(self):
response = self._make_request("GS {pin}\n".format(pin=self.number))
if response == "1":
return 1
else:
return 0
def _set_pull(self, value):
pass
def _set_edges(self, value):
print("STUB: set edges called: {0}".format(value))
if self._edges == "none" and value != "none":
self._thread = Thread(target=edge_detector,args=(self,))
self._thread.start()
if self._edges != "none" and value == "none":
self._edges = value;
self._thread.join()
self._edges = value
pass
def _get_when_changed(self, value):
return self._when_changed
def _set_when_changed(self, value):
print("STUB: set when changed: {0}".format(value))
self._when_changed = value
def _set_state(self, value):
response = self._make_request("SS {pin} {value}\n".format(pin=self.number, value=value))
if response != "OK":
raise Exception("Not understood", response)
The file defines StubPin
which extends Pin
from gpiozero. It defines all functions that was mandatory to be overriden. It also contains very naive edge detection as it was needed for gpio.Button
to work.
Let's make a demo :). Let's create virtualenv which gpiozero and my package installed:
$ virtualenv -p python3 rpi_stub_env
[...] // virtualenv successfully created
$ source ./rpi_stub_env/bin/activate
(rpi_stub_env)$ pip install gpiozero
[...] // gpiozero installed
(rpi_stub_env)$ python3 setup.py install
[...] // my package installed
Now let's create stub controller (open in other terminal etc.):
(rpi_stub_env)$ stub_rpi_controller /tmp/socket.sock
I will use the following script example.py
:
from gpiozero import Button
from time import sleep
button = Button(2)
while True:
if button.is_pressed:
print("Button is pressed")
else:
print("Button is not pressed")
sleep(1)
Let's execute it:
(rpi_stub_env)$ RPI_STUB_URL=/tmp/socket.sock GPIOZERO_PIN_FACTORY=stub_rpi python example.py
By default the script prints that the button is pressed. Now let's press the button:
(rpi_stub_env)$ stub_rpi_trigger /tmp/socket.sock SS 2 1
Now the script should print that the button is not pressed. If you execute the following command it will be pressed again:
(rpi_stub_env)$ stub_rpi_trigger /tmp/socket.sock SS 2 0
I hope it will help you.