New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Idea: create gpiozero yaml interface #548

Open
bennuttall opened this Issue Mar 21, 2017 · 10 comments

Comments

Projects
None yet
2 participants
@bennuttall
Member

bennuttall commented Mar 21, 2017

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.

@lurch

This comment has been minimized.

Show comment
Hide comment
@lurch

lurch Mar 21, 2017

Contributor

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 ;-)

Contributor

lurch commented Mar 21, 2017

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 ;-)

@bennuttall

This comment has been minimized.

Show comment
Hide comment
@bennuttall

bennuttall Mar 25, 2017

Member

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()
Member

bennuttall commented Mar 25, 2017

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

This comment has been minimized.

Show comment
Hide comment
@bennuttall

bennuttall Mar 26, 2017

Member

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)
Member

bennuttall commented Mar 26, 2017

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)
@lurch

This comment has been minimized.

Show comment
Hide comment
@lurch

lurch Mar 26, 2017

Contributor

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).

Contributor

lurch commented Mar 26, 2017

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).

@bennuttall

This comment has been minimized.

Show comment
Hide comment
@bennuttall

bennuttall Mar 26, 2017

Member

True

Member

bennuttall commented Mar 26, 2017

True

@bennuttall

This comment has been minimized.

Show comment
Hide comment
@bennuttall

bennuttall Apr 23, 2017

Member

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.

Member

bennuttall commented Apr 23, 2017

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.

@lurch

This comment has been minimized.

Show comment
Hide comment
@lurch

lurch Apr 24, 2017

Contributor

...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.

Contributor

lurch commented Apr 24, 2017

...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.

@bennuttall

This comment has been minimized.

Show comment
Hide comment
@bennuttall

bennuttall Aug 21, 2017

Member

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.

Member

bennuttall commented Aug 21, 2017

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.

@lurch

This comment has been minimized.

Show comment
Hide comment
@lurch

lurch Aug 21, 2017

Contributor

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".

Contributor

lurch commented Aug 21, 2017

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".

@bennuttall

This comment has been minimized.

Show comment
Hide comment
@bennuttall

bennuttall Sep 4, 2017

Member

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.

Member

bennuttall commented Sep 4, 2017

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment