Mittwoch, 22. November 2023

REST-API mit Python und hug für Pimoroni Blinkt LED Leiste erstellen

In diesen kleinen Projekt wird mit Hilfe des Python Webframeworks hug eine einfache webbasierte REST-API zum Ansteuern der Blinkt LED-Leiste von Pimoroni für den Raspbery Pi erstellt.

Das ganze hat keinen speziellen Grund, es ist für mich aber eine gute Gelegenheit, mal etwas mit hug zu erstellen. Wollte ich schon länger, es hatte sich bis dato aber keine Gelegenheit ergeben.

Ziel

Ziel des Programmierprojekt ist es, eine webbasierte API / Steuerung für die Blinkt-Leiste zu erstellen, genutzt werden dafür nur GET-Requests. Konkret bedeutet dies, das z.B. über den Aufruf der URL http://ip_adresse:port/color/red alle acht LEDs des Blinkt auf rot zu stellen. Die LEDs der Blinkt Leiste werden von Pimoroni übrigens "Pixel" genannt, dieser Begriff wird auch im weiteren hier verwendet.

Die API soll die folgenden Funktionen bieten:

  • alle Pixel auf eine vordefinierte Farbe stellen
  • Helligkeit für alle Pixel einstellen
  • für jedes der Pixel eine separate Farbe einstellen
  • für jedes Pixel eine separate Farbe einstellen, aber alle acht Farben auf einmal als Parameter übergeben
  • allen Pixeln eine zufällig gewählte Farbe zuweisen
  • Blinkt Leiste ausschalten

Die Farben sollen sich für diese Projekt nicht frei einstellen lassen, sondern es gibt sieben vordefinierte Farben: rot, orange, gelb, grün, blau, indigo, violett sowie weiß. Was den Farben das Regenbogens plus weiß entspricht.

Hardware

Als Hardware kommen eine Raspberry Pi Zero W sowie die Pimoroni Blinkt Leiste zum Einsatz. Die Blinkt Leiste wird direkt auf die GPIO Pins des Raspberry Pi gesteckt. Natürlich lässt sich das Projekt auch mit jedem anderen Raspberry umsetzen, solange dieser die 40 GPIO Pins hat Auf ganz alte Raspis mit nur 28 Pins passt die Leiste nicht.

Pi Zero im Pibow Gehäuse mit aufgesteckter Blinkt LED Leiste

 Software

Als Software kommt zum Einsatz:

  • Raspberry Pi OS. Hier wird die zum Zeitpunkt des Schreibens aktuelle Version "Bookworm" verwendet, das Projekt funktioniert aber auch mit älteren Versionen.
  • venv zum Erstellen eines virtual Environments für Python, in dem das Programm später läuft
  • innerhalb des venvs pip, der Paketmanager für Python
  • das Python Modul für hug, installiert via pip
  • das Python Modul für Blinkt, installiert aus den Paketquellen von Raspberry Pi OS
  • waitress als WSGI-Server, installiert via pip
  • systemd zum Erstellen einer Service Unit zum automatischen Starten des Server. systemd ist bei Raspberry Pi OS standardmäßig an Bord.

Anstellen von waitress kann natürlich auf jeder andere WSGI-Applikationsserver genutzt werden, wie z.B. Gunicorn. Beim Entwickeln bzw. zum Testen kann man auch den Server nutzen, den hug mitbringt - habe ich auch in der Testphase gemacht.

Programm

Vorbereitung

Als erstes wird die benötigte Software installiert und das venv angelegt sowie aktiviert.

Aus den Paketquellen von Raspberry Pi OS wird folgendes installiert:

sudo apt install python3-venv
sudo apt install python3-blinkt

Dann wird ein venv Names "hug_blinkt" angelegt. Ich habe dieses bei mir im Homeverzeichnis im Unterverzeichnis "code" angelegt, so dass der volle absolute Pfad /home/noisefloor/code/hug_blinkt lautet. Von Prinzip her geht aber natürlich auch jedes andere Verzeichnis, für das man ausreichende Rechte hat.

python3 -m venv hug_blinkt --system-site-packages

Die Option --system-site-packages ist deshalb notwendig, weil das Python-Modul für Blinkt via APT systemweit installiert ist, aber systemweite Pakete standardmäßig nicht in ein venv übernommen werden.

Jetzt ins venv wechseln, dieses aktivieren und darin die Python-Module für hug und waitress installieren:

cd hug_blinkt
source bin/activate
pip3 install hug
pip3 install waitress

Nun ist alles bereit, um das Programm zu erstellen und starten.

Quellcode

Der komplette Quellcode für das Programm sieht wie folgt aus:

import random
import hug
import blinkt


COLORS = {'red': (255, 0 , 0),
          'green': (0, 255, 0),
          'blue': (0, 0, 255),
          'yellow': (255, 255, 0),
          'orange': (255, 165, 0),
          'violet': (255, 87, 51),
          'indigo': (75, 0, 130),
          'white': (255, 255, 255),
          }

#define short name for each color, which is the first letter of the color
#as defined in COLORS
#needs to be modified / removed once two colors with the same starting letter
#are defined in COLORS, e.g. if ever purple and pink would be added.
COLORS_SHORT = {key[0]:value for (key, value) in COLORS.items()}

ALL_COLORS = COLORS | COLORS_SHORT

@hug.get('/color/{color}')
@hug.get('/color', examples='color=red')
def set_all_pixels(color: str):
    '''Sets all pixels to the the given color. The color is specified by name,
    e.g. red or green. Color can also be given by shortname, e.g. y for yellow
    or m for magenta.'''
    if not color in ALL_COLORS.keys():
        return {'error': f'{color} is not a valid color. Please choose one of \
{list(ALL_COLORS.keys())}'}
    else:
        rgb_value = ALL_COLORS[color]
        blinkt.set_all(rgb_value[0], rgb_value[1], rgb_value[2])
        blinkt.show()
        return {'success': f'set Pixels to color {color} (RBG: {rgb_value})'}

@hug.get('/color_for_pixel/{pixel}/{color}')
@hug.get('/color_for_pixel', examples='pixel=2&color=magenta')
def set_pixel_to(pixel: hug.types.in_range(1, 9), color: str):
    '''Sets the given Pixel to the given color. Pixels are numbered 1 to 8 from
    left to right. The color is specified by name, e.g. green or blue. Color can
    also be given by shortname, e.g. y for yellow or m for magenta.'''
    if not color in ALL_COLORS.keys():
        return {'error': f'{color} is not a valid color. Please choose one of \
{list(ALL_COLORS.keys())}'}
    else:
        pixel-=1
        rgb_value = ALL_COLORS[color]
        blinkt.set_pixel(pixel, rgb_value[0], rgb_value[1], rgb_value[2])
        blinkt.show()
        return {'success': f'set Pixel {pixel} to color {color} (RBG: {rgb_value})'}

@hug.get('/colors_for_pixels/{colors}')
@hug.get('/colors_for_pixels', examples='colors=green,red,yellow,magenta,white,blue,y,g')
def set_pixels_to(colors: hug.types.delimited_list(',')):
    '''Set each pixel to the given color specified for each Pixel. The colors
    are specified by name,     e.g. blue or yellow. In total eight colors needs
    to be given, one for each Pixel. Colors must     be separated by a comma , .'''
    if len(colors) != 8:
        return {'error': 'wrong length - exactly eight (8) colors separated by \
a comma have to be provided'}
    if not all(color.strip() in ALL_COLORS for color in colors):
        return {'error': f'There is at least one invalid value in {colors}. \
            Please ensure all values are one of {ALL_COLORS.keys()}'}
    _set_color_for_each_pixel(colors)
    return {'success': f'set Pixels 1 to 8 to colors {colors}'}

@hug.get('/brightness/{value}')
@hug.get('/brightness')
def set_brightness(value: hug.types.in_range(1, 100), 
                   examples='value=50'):
    '''Sets the brightness of all Pixels to the given value. Value must be an
    integer between 1 and 99'''
    value = round(value/100, 2)
    blinkt.set_brightness(value)
    blinkt.show()
    return {'success': f'set brightness to: {value}'}

@hug.get('/randomize')
def randomize_colors():
    '''Sets all eight pixels to a randomly chosen color from the color dict.'''
    colors = random.choices(list(COLORS.keys()), k=8)
    _set_color_for_each_pixel(colors)
    return {'success': 'set pixels to randomly chosen colors.'}

@hug.get('/show_state')
def show_state():
    '''Shows the current state of each pixel - current RGB value and brightness.'''
    state = {}
    for pixel in range(blinkt.NUM_PIXELS):
        value = blinkt.get_pixel(pixel)
        state[f'Pixel {pixel}'] = f'color: {value[0:3]}, brightness: {value[3]}' 
    return state

@hug.get('/off')
def leds_off():
    '''Turns off all pixels.'''
    blinkt.clear()
    blinkt.show()
    return {'success': 'turned off all Pixels'}

def _set_color_for_each_pixel(colors):
    '''Helper function to set each pixel to a given color.
    Expects one argument, which has to be an iterable holding the names of exactly
    eight colors.'''
    assert len(colors)==8
    rgb_values = [ALL_COLORS[color] for color in colors]
    for position, rgb_value in enumerate(rgb_values):
        blinkt.set_pixel(position, rgb_value[0], rgb_value[1], rgb_value[2])
    blinkt.show()

set_all_pixels(color: str) bindet die Route color an die Funktion set_all_pixels. Die Route und die daran gebundene Funktion erwarten ein Parameter Namens color, welches auf zwei Wege übergeben werden kann: color?color=red oder color/red.

Im Wörterbuch COLORS werden die erlaubten Farben definiert, was, wie weiter oben bereits erwähnt, die Regenbogenfarben plus weiß sind. Außerdem wird daraus das Wörterbuch COLORS_SHORT abgeleitet. Darin ist definiert, dass auch nur der Anfangsbuchstabe jeder Farbe als Farbangabe genutzt werden darf, also z.B. y für yellow. Alle Funktionen, denen ein oder mehrere Farben übergeben werden, prüfen, ob die Farbangabe bzw. die Farbangaben gültig sind, sprich im Wörterbuch ALL_COLORS vorkommen.

Alle Funktionen, die an eine Route gebunden sind, liefern als Antwort JSON zurück. Dies ist der Standard von hug. Das Wörterbuch nach dem return wird von hug in ein JSON-Objekt umgewandelt und mit dem passenden Content-Type Header versehen. hug kennt außer JSON aber natürlich auch noch andere Ausgabeformate.

Was meines Erachtens bei hug ganz praktisch ist sind die Type Annotations, welche man optional bei den Route angeben kann. Diese können ganz gewöhnlich sein wie bei color: str . Aber hug kennt auch weiterführende, komplexere Annotations. Hier im Programm verwendet werden zwei weitere:
value: hug.types.in_range(1, 100) prüft und stellt sicher, dass für value nur Zahlen zwischen 1 und 99 akzeptiert werden.
colors: hug.types.delimited_list(',') stellt sicher, dass für colors nur durch ein Komma separierte Liste von Werten akzeptiert wird, wie z.B. red,yellow,blue. Werte wie red;yellow;blue würden zu einem Fehler führen. Nach erfolgreicher Prüfung wandelt hug die Werte praktischerweise direkt in eine Python-Liste um.
Eine tiefer gehende Erklärung zu den Type Annotations in hug sind in der Doku zu finden.

Programm testen

Zum Testen des Programms benötigt man keinen WSGI Applikationsserver, sondern kann den in hug enthaltenen Server nutzen. Diesen ruft man mit

hug -f name_des_programms.py

auf. Danach ist das Programm bzw. die darüber bereit gestellt Web-API unter der Adresse 127.0.0.1:8000 auf dem Rechner, auf dem das Programm läuft, erreichbar.

Deployment

Das fertige Programm sollte zur produktiven / dauerhaften Nutzung - selbst wenn es nur im Heimnetzwerk ist - über einen WSGI-Server ausgeliefert werden, um einen stabilen Betrieb zu gewährleisten. Wie weiter oben bereits erwähnt nutze ich dafür waitress.

Der Befehl zum Starten von waitress innerhalb des venvs lautet:

waitress-serve --threads=2 --listen=192.168.178.82:8001 blinkt_api:__hug_wsgi__

Die Angaben für --listen müssen natürlich auf die IP-Adresse des Host-Rechners angepasst werden. Ebenso können der Port sowie die Anzahl der Threads geändert werden. Der Einstiegspunkt von hug für den WSGI-Server __hug_wsgi__ erhält man automatisch über das import hug im Skript.

Wer das Programm beim Systemstart automatisch starten will kann dies auf Basis der folgenden systemd Service Unit machen:

[Unit]
Description=Blinkt Server using hug as the backend
After=network-online.target

[Service]
User=DEIN_BENUTZERNAME
WorkingDirectory=/PFAD/ZUM/HUG/VENV
ExecStart=/PFAD/ZUM/HUG/VENV/bin/waitress-serve --listen=192.168.178.117:8001 --threads=2 NAME_DES_SKRIPTS:__hug_wsgi__
Restart=always

[Install]
WantedBy=multi-user.target

Der Pfad zum angelegten venv muss entsprechend eingetragen werden. Da hug, waitress sowie das Python-Modul für Blinkt keine speziellen Rechte brauchen kann das ganze ohne Probleme mit Nutzerrechten laufen, Root-Rechte sind nicht notwendig.

abschließende Worte

Wie schon Eingangs erwähnt habe ich das ganze Projekt primär gestartet, um mal etwas mit hug zu machen, was auch einen halbwegs produktiven Nutzen hat. Die Nutzung von hug ist ziemlich einfach und gradlinig. Ein bisschen komisch ist bei hug nur, dass die Doku auf zwei Webseiten verteilt ist, nämlich https://www.hug.rest/website/learn/ und https://hugapi.github.io/hug/ 

hug kann übrigens nicht nur webbasierte REST-APIs, sondern auch APIs für die Kommandozeile. Und hug hat noch wesentlich mehr Funktionen und Möglichkeiten als die hier gezeigten / genutzten.

Das hier gezeigt Programm ist natürlich auch nicht auf die Nutzung in Kombination mit der Blinkt LED-Leiste beschränkt. Das Prinzip lässt sich leicht auch auf andere Hardware übertragen, für die es ein Python-Modul zu Ansteuerung gibt.

Mit der Blinkt LED Leiste lässt sich übrigens sehr gut und einfach eine DIY Ambilight Beleuchtung hinter einem Monitor, Fernseher oder ähnlichem realisieren. Dazu einfach einen Raspberry Pi Zero W mit aufgesteckter Blinkt LED-Leiste hinter einem Monitor anbringen und die Pixel der Blinkt Leiste über eine Web REST-API wie hier gezeigt ansteuern.

Sonntag, 12. November 2023

GNU Nano Editor unter Windows intstallieren

Neulich war ich im Terminal von Windows unterwegs und wollte mal schnell eine einfache Änderung in einer Textdatei machen. Unter Linux wäre das ein Fall für den Nano Editor gewesen - unter Windows gibt es nach meinem Wissen im Terminal aber keinen vorinstallierten, textbasierten Editor. Also Google befragt, was es so gibt. Und, siehe da, es gibt tatsächlich Nano nicht nur für Linux, sondern auch für Windows.

Installieren lässt sich Nano auch direkt im Terminal über eine Paketverwaltung für Windows namens winget. Das ist unter Windows 11 und den neueren Versionen von Windows 10 normalerweise vorinstalliert. Wenn nicht kann man es einfach nachinstallieren, eine Anleitung und weitere Informationen sind auf der Infoseite von Microsoft zu finden.

Die Installation ist ganz einfach: einfach im Terminal den Befehl

winget install -e --id GNU.Nano 

ausführen und die aktuelle Nanio-Version wird installiert.

Über winget lässt sich auch noch andere Software aus dem GNU Projekt installieren, wie z.B. der Editor Emacs oder der textbasierte Dateimanager Midnight Commander. Eine Übersicht über die installierbare Software des GNU Projekt ist auf dieser Seite zu finden.

Leider ist es mir (bis jetzt) nicht gelungen die Stelle herauszufinden, wo man die Konfigurationsdatei .nanorc ablegen muss, um Nano Konfigurationsoptionen wie automatisches Einrücken, Zeilennummern, Leerzeichen statt Tab usw. mitzugeben.

Nano ist sicherlich nicht der beste Editor für Windows, aber für einen schnellen Edit im Terminal bzw. in der Powershell, ohne diese zu verlassen, gut genug.

Nano in der Windows Powershell