3

I have a python script running on my RPi. It uses the Gpiozero library (that's really great by the way).

For testing purposes i was wondering if it was possible to emulate GPIO states somehow (say emulate pressing a button) and have it picked up by the gpiozero library.

Thanks !

Malcoolm
  • 478
  • 2
  • 17

1 Answers1

3

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.

luantkow
  • 2,809
  • 20
  • 14