#! code: Drupal 11: Controlling LED Lights Using A REST Service

After some experimentation, I found that the most effective way to turn off caching for a RESTful resource that will be consumed by anonymous users is to return a new DrupalrestModifiedResourceResponse object. This acts in the same way as the ResourceResponse, but assumed that every response is unique and will therefore prevent the response from being cached. I have seen lots of people suggest setting the cache max-age setting to 0, but this doesn’t work for anonymous traffic.Following on from my article looking at the Pimoroni Plasma 2350 W I decided to do something interesting with the Wifi interface that would connect to a Drupal site.The power consumption of these lights is so tiny that I don’t mind leaving them on for a few weeks whilst people play with the colours. In fact, I left the lights plugged into a portable battery and they stayed on for about 4 days.public function submitForm(array &$form, FormStateInterface $form_state) {
$colour = $form_state->getValue('colour');
$this->setColourState($colour);
$form_state->setRebuild();

// Register flood handler.
$floodIdentifier = $this->getRequest()->getClientIp();
$this->flood->register('hashbangcode_plasma.plasma_flood_protection', self::ABUSE_WINDOW, $floodIdentifier);
}

The API system in Drupal is very powerful and comes with plugins for formats and authentication mechanisms out of the box. For the purposes of this project we only need the JSON formatter and the basic “cookie” authentication provider, which allows easy anonymous access to the resource.To define a RESTful API interface we first need to create a plugin class. This class will set the endpoint and create the code needed to power the interface. In addition to this, as our colour is stored in the state service we will use dependency injection to inject the service into the class.For completeness, here is the REST plugin class in full.langcode: en
status: true
dependencies:
module:
- hashbangcode_plasma
- serialization
- user
id: plasma
plugin_id: plasma
granularity: resource
configuration:
methods:
- GET
formats:
- json
authentication:
- cookie

To get this working we need to add the flood service to the form in much the same way as the state service was added.The submit handler for the form is pretty small. We just need to extract the colour value from the form state, use the setColourState() method that we set up at the start to write it to the state service, and then rebuild the form so that the new value appears in form after the page has reloaded.The original code that came with the Plasma 2350 W was actually part of a social media site integration where people could post colours on a platform and those colours would appear on the string of lights. The colour that was picked up from the original endpoint was the first colour in the string of LED lights.This all came together pretty quickly. Most of the work involved was setting things up in Drupal and deploying the code to the site as the MicroPython took just a few minutes. If you are new to RESTful resources in Drupal then you might not realise that you need the REST UI module installed for you to configure things. You can, of course, configure RESTful resources using hand crafted configuration files, but I tend to avoid doing that if possible.Drupal doesn’t have a built in interface to allow REST resources to be enabled or configured, but we can use the REST UI module to easily enable the Plasma resource and set the sorts of methods, formats, and authentications that it will use. With the REST UI module installed we can enable the Plasma resource and allow it to accept GET requests, which now looks like this.

Flood Protection

Now that we have a (protected) form we can build a REST resource to allow the colour to be displayed as a JSON feed.Now that I have this simple REST resource working I might extend it to include a brightness value or perhaps allow effects to be triggered using the form. It should be possible to implement the original intent of the LED lights by storing the colours in sequence and updating them in turn. The RESTful service then just needs to loop through the colours and print them out in the JSON feed.public function buildForm(array $form, FormStateInterface $form_state) {
$form['colour'] = [
'#type' => 'color',
'#title' => $this->t('Colour'),
'#default_value' => $this->getColourState(),
'#required' => TRUE,
];

$form['submit'] = [
'#type' => 'submit',
'#value' => 'Set Colour',
];

return $form;
}

Setting the form up with this service injected is simple enough, but we can also simplify the form integration by abstracting away the get and set methods for the state itself.import time

import urequests
from ezwifi import connect
from machine import Pin

import plasma

URL = "https://www.hashbangcode.com/plasma/[REDACTED]"
UPDATE_INTERVAL = 20 # refresh interval in secs

NUM_LEDS = 66

# wifi connection failed
def wifi_failed(message=""):
print(f"Wifi connection failed! {message}")

# Print out WiFi connection messages for debugging
def wifi_message(_wifi, message):
print(message)

def hex_to_rgb(hex):
# converts a hex colour code into RGB
h = hex.lstrip("#")
r, g, b = (int(h[i:i + 2], 16) for i in (0, 2, 4))
return r, g, b

# set up the Pico W's onboard LED
pico_led = Pin("LED", Pin.OUT)

# set up the WS2812 / NeoPixel™ LEDs
led_strip = plasma.WS2812(NUM_LEDS, color_order=plasma.COLOR_ORDER_BGR)

# start updating the LED strip
led_strip.start()

# set up wifi
try:
connect(failed=wifi_failed, info=wifi_message, warning=wifi_message, error=wifi_message)
except ValueError as e:
wifi_failed(e)

while True:
r = urequests.get(URL)
j = r.json()
r.close()

# flash the onboard LED after getting data
pico_led.value(True)
time.sleep(0.2)
pico_led.value(False)

# extract hex colour from the data
hex = j["colour"]

# and convert it to RGB
r, g, b = hex_to_rgb(hex)

# light up the LEDs
for i in range(NUM_LEDS):
led_strip.set_rgb(i, r, g, b)
print(f"LEDs set to {hex}")

# sleep
print(f"Sleeping for {UPDATE_INTERVAL} seconds.")
time.sleep(UPDATE_INTERVAL)

In order to allow the colour of the LED lights to be set I needed to create a form that would do just that.

Creating The REST Resource

{"colour":"#19bbbe"}

As I mentioned before, the firmware of the Plasma 2350 W from Pimoroni comes with a MicroPython script that connects to a free API that contains a colour. Every 120 seconds the MicroPython connects to this API, converts the HEX code it finds to RGB values, and updates the colour of the LED lights.What I didn’t want happening was that the form went live and people just started abusing it by changing the colour every few seconds. Thankfully, Drupal has a built in service called flood that gives us the ability to prevent abuse of the form by tracking the number of submits over a given period and comparing this against a threshold.This is what the basic structure of the form class looks like./**
* The flood service.
*
* @var DrupalCoreFloodFloodInterface
*/
protected FloodInterface $flood;

public static function create(ContainerInterface $container) {
$instance = new static();
$instance->state = $container->get('state');
$instance->flood = $container->get('flood');
return $instance;
}

namespace Drupalhashbangcode_plasmaForm;

use DrupalCoreFormFormBase;
use DrupalCoreFormFormStateInterface;
use DrupalCoreStateStateInterface;
use SymfonyComponentDependencyInjectionContainerInterface;

class SetColourForm extends FormBase {

/**
* The state service.
*
* @var DrupalCoreStateStateInterface
*/
protected StateInterface $state;

public static function create(ContainerInterface $container) {
$instance = new static();
$instance->state = $container->get('state');
return $instance;
}

/**
* Get the colour from the state service.
*
* @return string
* The colour.
*/
public function getColourState() {
return $this->state->get('hashbangcode_plasma.colour', '#ffffff');
}

/**
* Set the colour in the state interface.
*
* @param string $colour
* The colour to set.
*/
public function setColourState($colour) {
$this->state->set('hashbangcode_plasma.colour', $colour);
}

public function buildForm(array $form, FormStateInterface $form_state) {
// Add form here.
}

public function submitForm(array &$form, FormStateInterface $form_state) {
// Add submit here.
}

}

This will produce a configuration entity that we can export and write to our Drupal configuration directory and deploy upstream.public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);

$floodIdentifier = $this->getRequest()->getClientIp();
if (!$this->flood->isAllowed('hashbangcode_plasma.plasma_flood_protection', 30, 610, $floodIdentifier)) {
$form_state->setErrorByName('', $this->t('Too many uses of this form from your IP address. This address is temporarily banned. Try again later.'));
}
}

Normally when we return a response from a REST resource we would create a new DrupalrestResourceResponse object, which handles all of the upstream formatting that is done to transform the payload into a JSON string. For the purposes of this project that response type isn’t suitable since it would be cached by Drupal. As the Plasma 2350 W is making an anonymous request to the REST endpoint (so we don’t have to deal with authentication on the device) the updates made in the form wouldn’t be picked up.The firmware of the Plasma 2350 W from Pimoroni comes with an example that connects to a free API to update the colour randomly. Once I saw this in action I realised that it shouldn’t be too hard to convert that to pull the data from a Drupal REST service instead.The REST UI module configuration screen, showing the enabled Plasma resource.The form for this project just needs to accept the colour and allow the form to be submitted. Interestingly, Drupal comes with a “color” field type, which is a wrapper around the HTML input type of “color”. This means we don’t need to include any extra libraries to get the colour selection working, we can just add a single field with the appropriate type.Here is the contents of the get() method. # extract hex colour from the data
hex = j["colour"]

<?php

namespace Drupalhashbangcode_plasmaPluginrestresource;

use DrupalCoreStateStateInterface;
use DrupalCoreStringTranslationTranslatableMarkup;
use DrupalrestAttributeRestResource;
use DrupalrestModifiedResourceResponse;
use DrupalrestPluginResourceBase;
use SymfonyComponentDependencyInjectionContainerInterface;

/**
* Provides a resource for database watchdog log entries.
*/
#[RestResource(
id: "plasma",
label: new TranslatableMarkup("Plasma resource"),
uri_paths: [
"canonical" => "/plasma/[REDACTED]",
]
)]
class PlasmaResource extends ResourceBase {

/**
* The state service.
*
* @var DrupalCoreStateStateInterface
*/
protected StateInterface $state;

public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$instance = new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->getParameter('serializer.formats'),
$container->get('logger.factory')->get('rest')
);
$instance->state = $container->get('state');
return $instance;
}

public function get() {
$colours = [
'colour' => $this->state->get('hashbangcode_plasma.colour', '#ffffff'),
];

// Do not cache the response.
return new ModifiedResourceResponse($colours, 200);
}

}

To save the colour to the system we will use the state service, which is a handy little key/value service that allows us to write simple values to the database. This service is a good way of storing values that aren’t part of the Drupal configuration system. Ideally, you want values that can be easily recreated by the system if they don’t already exist. The colour setting is therefore an ideal candidate for the state service.{"colour":"#ffffff"}

/**
* Provides a resource for database watchdog log entries.
*/
#[RestResource(
id: "plasma",
label: new TranslatableMarkup("Plasma resource"),
uri_paths: [
"canonical" => "/plasma/[REDACTED]",
]
)]
class PlasmaResource extends ResourceBase {
}

The form to change the colour is available at the address https://www.hashbangcode.com/plasma/set-colour. Feel free to give it a go! I have been giving people the link to the colour setting form for a few weeks and some of my meetings have involved the lights behind me changing colour every few minutes. If I get enough interest I will set up a stream for a couple of days so that people can interact with it in a more real time waypublic function submitForm(array &$form, FormStateInterface $form_state) {
$colour = $form_state->getValue('colour');
$this->setColourState($colour);
$form_state->setRebuild();
}

public function get() {
$colour = [
'colour' => $this->state->get('hashbangcode_plasma.colour', '#ffffff'),
];

// Do not cache the response.
return new ModifiedResourceResponse($colour, 200);
}

In this article we will look at creating a Drupal module containing a RESTful interface, which we will connect to with the Plasma 2350 W to update the colour of the lights.It is quite easy to create RESTful services in Drupal; it just needs a single class. All I would need to do is create a form to control the colour that is selected in the REST service.URL = "https://www.hashbangcode.com/plasma/[REDACTED]"

What I needed to do was update the URL used, and reduce the amount of time between updates. This is just a case of altering the URL variable in the script.

Similar Posts