gpiozero icon indicating copy to clipboard operation
gpiozero copied to clipboard

Idea: create gpiozero yaml interface

Open bennuttall opened this issue 8 years ago • 10 comments

I had an idea and I've thrown together a proof-of-concept demo.

Imagine a YAML file which described a series of gpiozero objects to be used, and how they were connected. For example:

devices:
    - led: LED, 17
    - btn: Button, 18
connections:
    - led: btn

And imagine you could "import" that file somehow and it set up gpiozero objects for each device, and "connected" the devices using source/values. So this YAML file was just a shorthand way of describing the object behaviour.

I threw together this file which does just that:

import yaml
from gpiozero import *
from signal import pause

classes = {
    'LED': LED,
    'Button': Button,
}

def parse_config_file(config_file):
    with open(config_file, 'r') as f:
        return yaml.load(f)

def create_devices(config):
    devices = {}
    for device in config['devices']:
        for obj_name, data in device.items():
            class_name, *args = data.split(', ')
            ClassName = classes[class_name]
            devices[obj_name] = ClassName(*[int(arg) for arg in args])
    return devices

def setup_connections(config, devices):
    for connection in config['connections']:
        for source_device_ref, value_device_ref in connection.items():
            source_device = devices[source_device_ref]
            value_device = devices[value_device_ref]
            source_device.source = value_device.values

if __name__ == '__main__':
    config_file = 'led_button.yml'
    config = parse_config_file(config_file)
    devices = create_devices(config)
    setup_connections(config, devices)
    pause()

It's a bit of a hack right now, but I just wanted to see if it would work. Here's it working in a shell:

>>> %run gpiozero_yml.py
>>> devices
{'btn': <gpiozero.Button object on pin MOCK18, pull_up=True, is_active=False>,
 'led': <gpiozero.LED object on pin MOCK17, active_high=True, is_active=False>}
>>> devices['btn'].pin.drive_low()
>>> devices
{'btn': <gpiozero.Button object on pin MOCK18, pull_up=True, is_active=True>,
 'led': <gpiozero.LED object on pin MOCK17, active_high=True, is_active=True>}

Why?

I thought about a simple way to create or store a set of gpiozero "rules". Perhaps it could be used by a GUI to create Python code from blocks or something.

I think it could work with:

  • Creating gpiozero objects
  • Setting up events
  • Setting up source/values
  • Using source tools with source/values

I think it's limited to just these things, no other objects, no other code, no main loop - just set up events and source/values and pause. I think composite devices should work, but setting their source will be tricky if not trivially coded.

Note the YAML file could be more verbose, describing all the parameters properly, rather than splitting on a comma, but I considered this style to align with the simple nature of gpiozero, and of un-named, positional arguments. I think you could add keyword arguments easily enough too.

Todo:

  • Export to equivalent Python code
  • Add keyword arguments
  • Add events
  • Write a function to generate YAML from a Python file
  • Find a use for it?

Plus lots of cleanup like turn it into a class, automate the string: class dict, etc.

I might be mad to think this is useful. But I know better than not to share my mad ideas.

bennuttall avatar Mar 21 '17 00:03 bennuttall

I think the answer to "Find a use for it?" will be the key to seeing how useful such a feature would be, and what sort of direction it should be pushed in :)

Since the YAML

devices:
    - led: LED, 17
    - btn: Button, 18
connections:
    - led: btn

isn't really that much more succinct than the equivalent GpioZero code

from gpiozero import *
led = LED(17)
btn = Button(18)
led.source = btn.values

And obviously yaml->python conversion will be much easier than python->yaml conversion ;-)

lurch avatar Mar 21 '17 02:03 lurch

Put into a class and automated classes dict:

import yaml
import gpiozero
from signal import pause

classes = {
    name: cls
    for name, cls in gpiozero.__dict__.items()
    if isinstance(cls, type) and issubclass(cls, gpiozero.Device)
}

class GPIOZeroYamlParser():
    def __init__(self, config_file):
        with open(config_file, 'r') as f:
            self.config = yaml.load(f)

        self.devices = {}
        for device in self.config['devices']:
            for obj_name, data in device.items():
                class_name, *args = data.split(', ')
                ClassName = classes[class_name]
                self.devices[obj_name] = ClassName(*[int(arg) for arg in args])

        for connection in self.config['connections']:
            for source_device_ref, value_device_ref in connection.items():
                source_device = self.devices[source_device_ref]
                value_device = self.devices[value_device_ref]
                source_device.source = value_device.values

if __name__ == '__main__':
    parser = GPIOZeroYamlParser('led_button.yml')
    pause()

bennuttall avatar Mar 25 '17 00:03 bennuttall

Export to Python file:

class GPIOZeroYamlParser():
    def __init__(self, config_file):
        with open(config_file, 'r') as f:
            self.config = yaml.load(f)

        self.devices = {}
        self.device_data = {}
        self.classes = set()
        for device in self.config['devices']:
            for obj_name, data in device.items():
                class_name, *args = data.split(', ')
                ClassName = classes[class_name]
                self.classes.add(ClassName)
                self.devices[obj_name] = ClassName(*[int(arg) for arg in args])
                self.device_data[obj_name] = {
                    'class_name': class_name,
                    'args': [int(arg) for arg in args]
                }

        for connection in self.config['connections']:
            for source_device_ref, value_device_ref in connection.items():
                source_device = self.devices[source_device_ref]
                value_device = self.devices[value_device_ref]
                source_device.source = value_device.values

    def to_python(self):
        classes = ', '.join(obj.__name__ for obj in self.classes)
        devices = '\n'.join(
            '{} = {}({})'.format(obj_name, data['class_name'], *data['args'])
            for obj_name, data in self.device_data.items()
        )
        connections = '\n'.join(
            '{}.source = {}.values'.format(source_device, value_device)
            for connection in self.config['connections']
            for source_device, value_device in connection.items()
        )

        return """from gpiozero import {}
from signal import pause

{}

{}

pause()""".format(classes, devices, connections)

bennuttall avatar Mar 26 '17 00:03 bennuttall

Probably makes sense to have separate __init__(self, config_file), execute(self) and export_python(self, output_file) functions - and it should be possible to export_python() without having to execute() (i.e. don't actually touch the pins).

lurch avatar Mar 26 '17 01:03 lurch

True

bennuttall avatar Mar 26 '17 17:03 bennuttall

Just found this project (not particularly new) doing something similar with RPi.GPIO: https://github.com/projectweekend/Pi-Pin-Manager - but it's more trying to solve the boilerplate problem than create an interchange format. Nevertheless, interesting.

bennuttall avatar Apr 23 '17 23:04 bennuttall

...an interchange format...

Hmmm, I wonder if you could serialise a "current gpiozero session" (e.g. as created in the REPL) out to a YAML file, by iterating over all the in-use pins and outputting their "gpiozero state"; which would allow you to 'persist / recreate' a REPL session, without having to re-type or copy'n'paste all your current code? ;-) Obviously there'd be lots of things that wouldn't be supported, and it's just an off-the-cuff suggestion so maybe it's not possible anyway.

lurch avatar Apr 24 '17 15:04 lurch

Added JSON equivalent: https://gist.github.com/bennuttall/d34bcf7d01a186a7833d3f6e8a315c3d

If you only want Python output without touching the pins, just use mockpin pin factory.

I haven't added support for composite devices or source tools yet.

bennuttall avatar Aug 21 '17 10:08 bennuttall

Just occurred to me that storing strings like "LED, 17" (which you then have to 'manually' break apart again) feels like it's defeating the point of using a markup language? e.g. your current code does data.split(', ') which means this wouldn't work if it came across a string like "LED,17".

lurch avatar Aug 21 '17 10:08 lurch

True. It's a balance between not being too verbose in the markup. It should probably be a list of arguments, or even a dict (eugh), really, but I'm just being minimal.

bennuttall avatar Sep 04 '17 10:09 bennuttall