Idea: create gpiozero yaml interface
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.
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 ;-)
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()
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)
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).
True
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.
...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.
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.
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".
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.