pirShow

Alte Monitore sind zu Schade zum Entsorgen. Als Upcycling habe ich deshalb einen alten Monitor zu einem digitalen Bilderrahmen in meinem Flur umfunktioniert. Sinnvollerweise sollte er natürlich nur dann ein Bild zeigen, wenn auch jemand da ist, der es betrachten kann. Hier möchte ich einmal kurz beschreiben, wie ich einen Raspberry Pi, einen passiven Bewegungssensor und etwas Python-Code zu diesem Zweck benutze.

Bildquellen definieren

Die Hauptfunktionalität eines digitalen Bilderrahmens ist es natürlich Bilder anzuzeigen. Diese Bilder sollen aus mehreren Quellen zufällig ausgewählt werden. Dabei habe ich mir einige Flickr-Accounts über Raumfahrt und meine Twitter-Bots ausgesucht. Zuerst brauchen wir also etwas Code, um die Bilder herunterzuladen.

def flickr(user_id):
    import flickrapi
    from keys_and_secrets import keys_and_secrets

    url_template = 'http://farm%(farm_id)s.staticflickr.com/%(server_id)s/%(photo_id)s_%(secret)s_b.jpg'

    def url_for_photo(p):
        return url_template % {
            'server_id': p.get('server'),
            'farm_id': p.get('farm'),
            'photo_id': p.get('id'),
            'secret': p.get('secret'),
        }

    flickr = flickrapi.FlickrAPI(keys_and_secrets["flickr_key"], keys_and_secrets["flickr_secret"])

    photo = random.choice(flickr.photos.search(user_id=user_id, per_page=500)[0])
    purl = url_for_photo(photo)
    title = photo.get('title')

    fname = save_image(purl, title)

    return fname


def twitter(atname):
    import tweepy
    from keys_and_secrets import keys_and_secrets

    auth = tweepy.OAuthHandler(keys_and_secrets["consumer_key"], keys_and_secrets["consumer_secret"])
    auth.set_access_token(keys_and_secrets["access_token_key"], keys_and_secrets["access_token_secret"])

    api = tweepy.API(auth)
    tweets = api.user_timeline(atname, count=30)

    urls = []
    for i in tweets:
        if "media" in i.entities:
            for j in i.entities["media"]:
                url = j["media_url"]
                if "thumb" not in url:
                    urls.append((url, i.id_str))

    purl, title = random.choice(urls)

    fname = save_image(purl, title)

    return fname

Dann bauen wir uns einen praktischen Decorator, den wir nutzen, um unterschiedliche Accounts als Bildquellen zu registrieren.

sources = {}

def source(source_name):
    def source_decorator(func):
        sources[source_name] = func

        @wraps(func)
        def func_wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        return func_wrapper
    return source_decorator


@source("apollo")
def apollo():
    return flickr("projectapolloarchive")


@source("randomGraphs")
def randomGraphs():
    return twitter("@randomGraphs")


@source("AFractalADay")
def AFractalADay():
    return twitter("@AFractalADay")

Dies ermöglicht es dann sehr komfortabel zufällige Bilder herunterzuladen und anzuzeigen.

def random_image():
    image_getter = random.choice(list(sources.values()))
    fname = image_getter()
    # show image
    # skipped terminating old instance of feh and aquiring a mutex
    env = os.environ
    env["DISPLAY"] = ":0"
    VIEWER = subprocess.Popen(
        ["feh", "-FZYx", fname],
        env=env,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    )

Monitor ein- und ausschalten

Jetzt, da wir Bilder zum Anzeigen haben, müssen wir den Monitor ein- und ausschalten, damit man sie sieht bzw. damit wir nicht sinnlos Strom verbrauchen. Hier rufen wir wieder Kommandozeilen-Werkzeuge auf: tvservice schaltet den Standby-Modus des Monitors um und chvt wechselt einmal vom X-Server weg und wieder zurück, was den Bildschirmschoner beendet.

from threading import Timer, Lock

MUTEX = Lock()
STATE = False

def monitor(status):
    global STATE
    # needs to run as root
    # make sure that sudo will not ask for a password for these commands
    # e.g. use visudo to add
    # piruser ALL=(ALL) NOPASSWD: /usr/bin/tvservice, /bin/chvt
    with MUTEX:
        if status:
            if not STATE:
                os.system("sudo tvservice -p; sleep 0.5; sudo chvt 6; sleep 0.5; sudo chvt 7")
            STATE = True
        else:
            if STATE:
                os.system("sudo tvservice -o")
                # download and show the next image
                random_image()
            STATE = False

PIR

Jetzt müssen wir diese Funktionalität nur noch durch einen Bewegungssensor auslösen. Dazu schließen wir einfach einen Pyroelektrischen Infrarot Sensor (PIR) an beispielsweise Pin 23 und sagen dem Raspberry, dass er dort horchen soll, ob ein Signal anliegt.

Ein PIR Bewegungsensor, der an einen Raspberry Pi angeschlossen ist

import RPi.GPIO as GPIO

SENSOR_PIN = 23

GPIO.setmode(GPIO.BCM)
GPIO.setup(SENSOR_PIN, GPIO.IN)

if __name__ == "__main__":
    GPIO.add_event_detect(SENSOR_PIN, GPIO.RISING, callback=pir_callback)

Der pir_callback schaltet dann einfach den Monitor an und startet einen Timer, der den Monitor wieder ausstellt (dieser Timer wird abgebrochen sobald der Callback erneut aufgerufen wird, damit der Monitor an bleibt, solange jemand das Bild betrachtet.)

Der pir_callback sendet außerdem auch eine MQTT-Nachricht, um die Bewegungsmeldung auch für Home-Assistant-Automatisierungen zu nutzen, sodass die Beleuchtung im Flur nach Sonnenuntergang nun auch durch Bewegungen ausgelöst wird.

Künstliche Kunst

Seit der Vorstellung von DALL-E Anfang 2021 sind Text-zu-Bild-Programme im Bewusstsein der Öffentlichkeit angekommen. DALL-E 2.0 und Midjourney haben immer wieder mit interessanten, verrückten und überraschend gut aussehenden Bildern auf sich aufmerksam gemacht, die aber immer nur in sehr begrenztem Umfang von ausgewählten Usern auf Servern der Betreibern generiert werden konnten. Aber seit August 2022 gibt es mit Stable Diffusion das erste hochwertige Text-zu-Bild-Modell, dessen Neuronales Netzwerk offen ist und von jedem (der eine Grafikkarte mit genügend Speicher hat) auf dem eigenen Computer genutzt werden kann!

Die grundlegende Funktionsweise ist, dass man dem Modell eine Bildbeschreibung, sogenannte Prompts, gibt und das Modell versucht ein Bild zu rendern, das möglichst gut zu der Beschreibung passt. Die Kunst liegt nun darin, die Bildbeschreibung so zu formulieren, dass das resultierende Bild möglichst gut wird. Wenn wir wissen wollen, wie so eine künstliche Intelligenz überhaupt aussieht, können wir Stable Diffusion darum bitten uns ein Bild von einer zu malen mit dem Prompt „a painting of an artificial intelligence“:

a painting of an artificial intelligence

In der Community findet man häufig Prompts, die mit vielen Adjektiven (wie „intricate“ oder „highly detailed“) oder Künstlernamen (vor allem „Greg Rutkowski“) gespickt sind. Für mich persönlich klingt es nach zu viel Arbeit eine solche Liste von Schlüsselworten an eine knappe Bildbeschreibung zu hängen — ich benutze schließlich eine künstliche Intelligenz, die Bilder zeichnet, damit ich wenig Arbeit habe!

Die offensichtliche Lösung für dieses Problem ist es natürlich ein Sprachmodell zu benutzen, um Prompts zu generieren. Glücklicherweise gibt es mit lexica.art eine Datenbank von Prompts, die man nutzen kann, um ein GPT-2 Modell zu finetunen. So kann man GPT-2 Modell mit „four dimensional space whale“ füttern, GPT-2 macht daraus den Prompt „four dimensional space whale, with recursive spiral eyes, concept art, high detail, intimidating, cinematic, Artstation trending, octane render“, der von Stable Diffusion zu diesem Bild gerendert wird:

Ein vierdimensionaler Weltraumwal

Und damit ich auch keine Arbeit damit habe, die Bilder selbst zu generieren, habe ich einen Twitter-Bot damit beauftragt täglich ein lovecraft’sches Bild zu tweeten: @ACthulhuADay.

Der Glue-Code, der diesen text2prompt2image-Ablauf implementiert (also hauptsächlich Modelle von Huggingface herunterlädt) und diesen Bot antreibt, findet sich auf Github.

Analog-Digital-Analoges Thermometer

Ich habe mir ein analoges Voltmeter zugelegt und möchte es als Thermometer benutzen.

Da der Widerstand von Metallen mit der Temperatur steigt, kann man Temperatur relativ gut messen, indem man einen kalibrierten Widerstand misst. Daher kann man theoretisch mit einem Multimeter auch die Temperatur messen. (In der Praxis wird dies bei Multimetern allerdings in der Regel mithilfe eines anderen Effektes erledigt.)

Da ich mir aber keine Gedanken darüber machen möchte, wie ich eine Schaltung aussehen müsste, um \(15°\mathrm{C}\) in \(1.5 \mathrm{V}\) umzusetzen (vielleicht würde eine Brückenschaltung funktionieren?), wähle ich den einfachen Weg mit einer Reihe integrierter Schaltkreise und einem Microcontroller.

Schaltplan meines Analog-Digital-Analog-Thermometers

Hier ist ein günstiger DS18B20 Temperatursensor, der von einem ESP8266 ausgelesen wird. Dieser steuert dann einen MCP4725 Digital-Analog-Wandler so an, dass er eine Spannung ausgibt, deren Wert in Volt ein Zehntel der gemessenen Temperatur ist. Diese Spannung wird dann von meinem alten Voltmeter gemessen und angezeigt. Hier ist es also gerade \(24°\mathrm{C}\).

Foto meines Analog-Digital-Analog-Thermometers

Hier ist übrigens der simple Code, der beispielsweise mit der Arduino IDE auf einen ESP8266 geflasht werden kann:

#include <Wire.h>
#include <Adafruit_MCP4725.h>
#include <OneWire.h>
#include <DallasTemperature.h>

#define ONE_WIRE_BUS D4
#define MCP4725In A0

Adafruit_MCP4725 MCP4725;

OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature DS18B20(&oneWire);

void setup() {
    Serial.begin(9600);

    DS18B20.begin();
    // 0x60 is the I2C address of my MCP4725A0
    MCP4725.begin(0x60);
}

float getTemperature() {
    float temp;
    do {
      DS18B20.requestTemperatures();
      temp = DS18B20.getTempCByIndex(0);
      delay(100);
    } while (temp == 85.0 || temp == (-127.0));
    return temp;
}

void setVoltage(float value) {
    value /= 10;
    float voltageOut = value*4096/3.3;
    MCP4725.setVoltage(voltageOut, false);

    // read it for testing and maybe calibrating
    int adcInput = analogRead(MCP4725In);
    float voltageIn = (adcInput * 3.3 )/ 1024.0;
    Serial.print("Expected Voltage: ");
    Serial.println(value, 3);

    Serial.print("Measured Voltage: ");
    Serial.println(voltageIn, 3);
}

void loop() {

    float temperature = getTemperature();
    setVoltage(temperature);

    // send temperature to the serial console
    dtostrf(temperature, 2, 2, temperatureString);
    Serial.println(temperatureString);

    delay(1e3);
}