How to stream your Raspberry Pi camera (using PiCamera2) as mjpg

Views: 14

A Raspberry Pi with an attached PiCamera can work as a simple surveillance system. A very simple way to stream the images is using MJPEG.

To quickly set up streaming MJPEG, I created a Python script that encodes the images in separate threads. Therefore the whole CPU of the Pi can be utilized. The maximum framerate is equal to what the camera can deliver (47 frames). On a Pi 4 this makes the CPU quite hot, so the script allows to throttle the throughput. 25 FPS is easily possible without any cooling.

Prerequisites

A Raspberry Pi with an attached camera module. I tested this on a Pi 2, 3 and 4, it probably works on a Pi 1 just as well, albeit with a severely reduced framerate.

Install the latest version of the OS:

sudo apt update
sudo apt upgrade

Install necessary python modules

sudo apt install python3-flask python3-libcamera python3-picamera2 python3-opencv

And then run the script

python pystream.py

The stream will be available at port 9000 of your Raspberry Pi: http://your-pi-IP:9000/mjpg

If you run this in a terminal, the process will of course stop once the window is closed, unless you run it using screen. If you haven’t installed it yet:

sudo apt install screen

Start the screen by just typing

screen

A short explanation will show, you can end that with <RETURN>. Anything you start in this will continue to run, even if you close the window.

To get back to a running screen process, just type

screen -r

The script (download here):

#!/usr/bin/env python3

from flask import Flask, Response
import cv2
from picamera2 import Picamera2
import libcamera
import time
import threading

# Creates an mjpg stream from a PiCamera. It's a quick and dirty example how to use your Pi as a surveillance camera
# Uses 2 separate threads to encode the captured image to maximize throughput by using all 4 cores
# The way this "double-buffering" is implemented causes it to only work correctly with one client. 


# Set your desired image size here
# The timing for approximating the frames per second depends largely on this setting
# Larger images means more time needed for processing
CAM_X = 1280
CAM_Y = 720


# Change this, depending on the orientation of your camera module
CAM_HFLIP = False
CAM_VFLIP = False

# Change this to control the usage and therefore temperature of your Pi. On my Pi 4 a setting of 25 FPS
# results in CPU usage of roughly 40% and no temperature throttling (no additional cooling here)
# Set to 0 to impose no restrictions (on my Pi 4 this results in ~47 FPS (maximum of my PiCamera model 2), on my Pi 2 ~17 FPS)
MAX_FPS = 25

# Flask is our "webserver"
# The URL to the mjpg stream is http://my.server:WEB_PORT/mjpg
WEB_PORT = 9000
app = Flask(__name__)

# Keeps all data for the various threads in one place
class MgtData(object):
    stop_tasks = False            # If set, all threads should stop their processing
    frame1_has_new_data = False   # Is being set when frame1 receives a new buffer to encode
    lock1 = False                 # Is being set when frame1 receives a new buffer to encode, and cleared when encoding is done
    frame2_has_new_data = False   # Same for frame 2
    lock2 = False

    img_buffer1 = None            # Receives the image as a byte array for frame 1
    img_buffer2 = None            # ... for frame 2
    encoded_frame1 = None         # Stores the JPG-encoded image for frame 1
    encoded_frame2 = None         # ... for frame 2

    # If there is new data available on frame 1, return True
    def frame1_new_data():
        return (MgtData.frame1_has_new_data and not MgtData.lock1)

    # If there is new data available on frame 2, return True
    def frame2_new_data():
        return (MgtData.frame2_has_new_data and not MgtData.lock2)


# Deliver the individual frames to the client
@app.route('/mjpg')
def video_feed():
    generated_data  = gen()
    if (generated_data):
        response = Response(gen(), mimetype='multipart/x-mixed-replace; boundary=frame') 
        response.headers.add("Access-Control-Allow-Origin", "*")
        return response
    return None

# Generate the individual frame data
def gen():
    while not MgtData.stop_tasks:  
        while (not (MgtData.frame1_new_data() or MgtData.frame2_new_data())):
            time.sleep (0.01) # Wait until we have data from one of the encode-threads

        frame = get_frame()
        yield (b'--frame\r\n'
               b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')

# If one of the frames has data already processed, deliver the respective encoded image
def get_frame():
    encoded_frame = None
    if (MgtData.frame1_new_data() or MgtData.frame2_new_data()):
        if (MgtData.frame1_new_data ()):
            encoded_frame = MgtData.encoded_frame1
            MgtData.frame1_has_new_data = False
        elif (MgtData.frame2_new_data ()):
            encoded_frame = MgtData.encoded_frame2
            MgtData.frame2_has_new_data = False
    else:
        print ("Duplicate frame")

    return encoded_frame




# Start the server
def start_webserver():
    try:
        app.run(host='0.0.0.0', port=WEB_PORT, threaded=True, debug=False)
    except Exception as e:
        print(e)

# Definition for the encoding thread for frame 1
def encode1():
    newEncFrame = cv2.imencode('.jpg', MgtData.img_buffer1)[1].tobytes()
    MgtData.encoded_frame1 = newEncFrame
    MgtData.frame1_has_new_data = True
    MgtData.lock1 = False

# Definition for the encoding thread for frame 2
def encode2():
    MgtData.lock2 = True
    MgtData.frame2_has_new_data = True
    newEncFrame = cv2.imencode('.jpg', MgtData.img_buffer2)[1].tobytes()
    MgtData.encoded_frame2 = newEncFrame
    MgtData.lock2 = False



def run_camera():
    # init picamera
    picam2 = Picamera2()

    preview_config = picam2.preview_configuration
    preview_config.size = (CAM_X, CAM_Y)
    preview_config.format = 'RGB888'
    preview_config.transform = libcamera.Transform(hflip=CAM_HFLIP, vflip=CAM_VFLIP)
    preview_config.colour_space = libcamera.ColorSpace.Sycc()
    preview_config.buffer_count = 4 # Looks like 3 is the minimum on my system to get the full 47 FPS my camera is capable of
    preview_config.queue = True
    preview_config.controls = {'FrameRate': MAX_FPS and MAX_FPS or 100}

    try:
        picam2.start()

    except Exception as e:
        print(e)
        print("Is the camera connected correctly?\nYou can use \"libcamea-hello\" or \"rpicam-hello\" to test the camera.")
        exit(1)
    
    fps = 0
    start_time = 0
    framecount = 0
    try:
        start_time = time.time()
        while (not MgtData.stop_tasks):
            if (not (MgtData.frame1_new_data() and MgtData.frame2_new_data())):

                # get image data from camera
                my_img = picam2.capture_array()

                # calculate fps
                framecount += 1
                elapsed_time = float(time.time() - start_time)
                if (elapsed_time > 1):
                    fps = round(framecount/elapsed_time, 1)
                    framecount = 0
                    start_time = time.time()
                    print ("FPS: ", fps)

                # if one of the two frames is available to store new data, copy the captured image to the
                # respective buffer and start the encoding thread
                # At max we have 4 threads: our main thread, flask, encode1 and encode2
                if (not MgtData.frame1_new_data()):
                    MgtData.img_buffer1 = my_img
                    MgtData.frame1_has_new_data = True
                    MgtData.lock1 = True
                    encode_thread1 = threading.Thread(target=encode1, name="encode1")
                    encode_thread1.start()
                elif (not MgtData.frame2_new_data()):
                    MgtData.img_buffer2 = my_img
                    MgtData.frame2_has_new_data = True
                    MgtData.lock2 = True
                    encode_thread2 = threading.Thread(target=encode2, name="encode1")
                    encode_thread2.start()
            time.sleep (0.0005) # No need to constantly poll, cut the CPU some slack
            
    except KeyboardInterrupt as e:
        print(e)
        MgtData.stop_tasks
    finally:
        picam2.close()
        cv2.destroyAllWindows()



def streamon():
    camera_thread = threading.Thread(target= run_camera, name="camera_streamon")
    camera_thread.daemon = False
    camera_thread.start()

    if camera_thread != None and camera_thread.is_alive():
        print('Starting web streaming ...')
        flask_thread = threading.Thread(name='flask_thread',target=start_webserver)
        flask_thread.daemon = True
        flask_thread.start()
    else:
        print('Error starting the stream')

    while not MgtData.stop_tasks:
        time.sleep (25) # Just waiting to end this thread



if __name__ == "__main__":
    try:
        streamon()
    except KeyboardInterrupt:
        pass
    except Exception as e:
        print(e)
    finally:
        print ("Closing...")
        MgtData.stop_tasks = True

EMS Bus Dekodieren und Steuern

Views: 77

Meine alte Heizung ist ausgefallen. Die wurde von einer Junkers TRQ21 gesteuert – was nichts anderes war als ein Thermostat, der für eine Wärmeanforderung 24V auf die Steuerleitung legt.

Da bei mir der Thermostat in der Küche hängt, die am wenigsten geheizt wird, ist die Steuerung für meine anderen Räume suboptimal. Ich hatte den Thermostat weiter in Betrieb, damit der für Frostschutz funktioniert, aber ich hatte zusätzlich einen Raspberry Pi, der mittels eines einfachen Scheduling-Systems und einer simplen Weboberfläche den Thermostat überbrücken konnte, und einfach die volle Spannung auf die Steuerleitung legte, wenn ich es wärmer haben wollte.

Die neue Heizung, eine Bosch Condens 5300i, hat (leider) eine EMS Steuerung, was zwar technisch viel besser ist, aber es schwieriger macht, mit selbstgebauten Systemen eine Fernsteuerung umzusetzen.

Grundsätzlich kann der Raspberry Pi die Daten vom Bus lesen, er braucht aber einen Pegel von 0V (0) bzw. 3,3V (1). Der EMS-Bus liefert bei mir gemessen 10V (0) und 15V (1). Bei kbza.de fand ich den Bauplan für einen Pegelwandler, der jetzt bei mir auch so läuft. Ich habe nur für R1 10kΩ genommen, da meine Messungen ergaben, dass meine Schaltung schon bei etwas unter 10V einen HIGH Pegel ergab, mit 10kΩ statt 8,2kΩ schaltet es bei mir zuverlässig und das Ergebnissignal sieht auf dem Oszilloskop sehr gut aus.

Auf der Webseite ist auch ein Python-Skript, das allerdings für mich nicht funktioniert. Das Skript selber ist auch nicht ein Beispiel für gute Programmierung, aber immerhin ein Hinweisgeber.

Das Skript erkannte keine der Nachrichten, die bei mir vom EMS Bus kommen. Eine extrem vereinfachte Form liefert die eingehenden Daten als Hexwerte:

#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-

import time
import serial
import sys

ser = serial.Serial(
 port='/dev/ttyAMA0',
 baudrate = 9600,
 parity=serial.PARITY_NONE,
 stopbits=serial.STOPBITS_ONE,
 bytesize=serial.EIGHTBITS,
 timeout=1
)

while 1:
    char = ser.read()
    hex = char.encode('HEX')
    sys.stdout.write(hex + ' ');
    sys.stdout.flush()

Bei mir kommt dann z.B. folgendes raus:

89 00 08 00 88 00 09 00 89 00 12 00 18 00 98 00 08 00 88 00 08 00 88 00 13 00 09 00 89 00 08 00 88 00 09 00 89 00 14 00 18 00 98 00 08 00 88 00 08 00 88 00 15 00 09 00 89 00 08 00 88 00 09 00 89 00 16 00 18 00 98 00 08 00 88 00 08 00 88 00 17 00 09 00 89 00 08 00 88 00 09 00 89 00 20 00 18 00 98 00 08 00 88 00 08 00 88 00 28 00 09 00 89 00 08 00 88 00 09 00 89 00 30 00 18 00 98 00 08 00 88 00 08 00 88 00 38 00 09 00 89 00 08 00 88 00 09 00 89 00 40 00 18 00 98 00 08 00 88 00 08 00 88 00 48 00 09 00 89 00 08 00 88 00 09 00 89 00 50 00 18 00 98 00 08 00 88 00 08 00 88 00 58 00 09 00 89 00 08 00 88 00 09 00 89 00 60 00 18 00 98 00 08 00 88 00 08 00 88 00 68 00 09 00 89 00 08 00 88 00 09 00 89 00 0a 00 18 00 98 00 08 00 88 00 08 00 88 00 0b 00 09 00 89 00 08 00 88 00 09 00 89 00 0c 00 18 00 98 00 08 00 88 00 08 00 88 00 0d 00 09 00 89 00 08 00 88 00 09 00 89 00 0e 00 18 00 98 00 08 00 88 00 08 00 88 00 0f 00 09 00 89 00 08 00 88 00 09 00 89 00 10 00 18 00 98 00 08 00 88 00 08 00 88 00 11 00 09 00 89 00 08 00 88 00 e4 00 10 20 2d 2d 00 c8 40 02 84 64 12 03 00 02 6e 00 00 80 00 00 87 0f 00 02 83 00 00 02 00 88 00 e4 23 00 01 54 00 9a 12 64 f9 00 88 00 e3 00 01 00 01 00 00 00 00 00 00 00 00 02 84 12 64 46 00 40 00 00 0e 00 88 00 09 00 89 00 12 00 18 00 98 00 08 00 88 00 e9 00 00 80 00 80 00 00 00 00 00 46 3c 00 00 00 00 10 ba 00 01 cf 00 00 00 05 02 00 3c 00 88 00 08 00 88 00 13 00 09 00 89 00 08 00 88 00 09 00 89 00 14 00 18 00 98 00 08 00 88 00 08 00 88 00 15 00 09 00 89 00 08 00 88 00 09 00 89 00 16 00 18 00 98 00 08 00 88 00 08 00 88 00 17 00 09 00 89 00 08 00 88 00 09 00 89 00 19 00 18 00 98 00 08 00 88 00 08 00 88 00 21 00 09 00 89 00 08 00 88 00 09 00 89 00 29 00 18 00 98 00 08 00 88 00 08 00 88 00 31 00 09 00 89 00 08 00 88 00 09 00 89 00 39 00 18 00 98 00 08 00 88 00 08 00 88 00 41 00 09 00 89 00 08 00 88 00 09 00 89 00 49 00 18 00 98 00 08 00 88 00 08 00 88 00 51 00 09 00 89 00 08 00 88 00 09 00 89 00 59 00 18 00 98 00 08 00 88 00 08 00 88 00 61 00 09 00 89 00 08 00 88 00 09 00 89 00 69 00 18 00 98 00 08 00 88 00 08 00 88 00 0a 00 09 00 89 00 08 00 88 00 09 00 89 00 0b 00 18 00 98 00 08 00 88 00 08 00 88 00 0c 00 09 00 89 00 08 00 88 00 09 00 89 00 0d 00 18 00 98 00 08 00 88 00 08 00 88 00 0e 00 09 00 89 00 08 00 88 00 09 00 89 00 0f 00 18 00 98 00 08 00 88 00 08 00 88 00 10 00 09 00 89 00 08 00 88 00 09 00 89 00 11 00 18 00 98 00 08 00 88 00 08 00 88 00 12 00 09 00 89 00 08 00 88 00 09 00 89 00 13 00 18 00 98 00 08 00 88 00 08 00 88 00 14 00 09 00 89 00 08 00 88 00

Nichts sieht so aus wie die “Header”, die das Originalskript suchte. Den Beschreibungen im EMS Wiki scheine ich auch nicht zu verstehen. Wenn ich mir nur 08 00 88 00 09 00 89 00 ansehe, macht das im Moment keinen Sinn. Was ich glaube zu verstehen ist dass eine Nachricht, in der das oberste Bit in der Empfänger-ID gesetzt ist, gefolgt von einem BREAK, eine dringende Anfrage an die entsprechende ID ist. Die Antwort – wenn es nichts zu senden gibt – ist die ID ohne das oberste Bit, gefolgt von einem BREAK.

08 ist eine ID – wenn ich das richtig verstehe sollte das die ID des Brenners sein. 00 kann ein Break sein, oder eine echte 0. Danach aber kommt 88 (= 08 + bit 7) gefolgt von 00.

Was ist ein BREAK frage ich mich da – das ist ein Byte mit Wert 0 und nicht gesetztem Stop-Bit, oder auch: die Leitung liegt relativ lange auf 0. Das serial Modul von Python kann diese Unterscheidung nicht liefern, allerdings kann der PL011 UART im Raspberry Pi das erkennen.

Mit cat /proc/tty/driver/ttyAMA kann man die Anzahl von Breaks sehen, die entdeckt wurden während Daten vom seriellen Port gelesen wurden:

0: uart:PL011 rev2 mmio:0x20201000 irq:81 tx:0 rx:2663 brk:1154 oe:1 CTS

Im Schnitt kam es also etwa auf 2 1/2 empfangenen Bytes ein Break. Wenn ich die Daten oben genauer ansehe, dann scheint es oft zu sein, dass eine ID (mit und ohne 7. Bit) kommt, dann 00 gelesen wird, dann wieder ID und 00. Dann gibt es noch ein paar Bereiche, die mehr nach sinnvollen Daten aussehen. Grob geschätzt würde ich glauben, dass fast alle als 00 gelesene Bytes in Wirklichkeit Breaks sind.

Natürlich könnte ich mir bei BBQKees Electronics ein EMS-Gateway kaufen, das soll alles wichtige können, aber ich würde gerne das Problem für mich selber lösen.

Also: wie komme ich an die Info vom UART, dass wir einen Break und keine 0 haben?

Home Automation für mich – erste Schritte

Views: 274

ZigBee ist also ein Funkprotokoll, mit dem IoT-Geräte sich mitteilen können. Es ist nicht WiFi, also muss ich den Geräten auch nicht einen potentiellen Zugang zum Internet bieten. Es ist ein etablierter Standard, also erwarte ich, dass das auch funktioniert.

ZigBee benötigt eine Empfangsstation, also habe ich mir einen Sonoff ZigBee 3.0 USB Stick sowie einen ESSENTIALS Heizkörper-Thermostat Zigbee gekauft. Ich empfehle Geräte grundsätzlich nur und dann auch explizit, wenn ich der Meinung bin, dass sie gut sind. Diese beiden Geräte erwähne ich nur, weil ich jetzt mit denen angefangen habe.

Weil ich dachte, bzw. eher nur hoffte, dass wir vielleicht mit Heinautomatisierung bereits an wirklich vernünftigen Standards angekommen sind, bin ich erstmal davon ausgegangen, dass ich einfach Home Assistant installieren könne und alles funktioniert wie Magie.

Home Assistant kann unter anderem mittels HAOS (Home Asssistant Operating System) direkt auf einem Raspberry Pi ausgeführt werden, im Idealfall braucht es nur etwas Zeit und dann ein Pairing mit allen Geräten. Benutzt man HAOS, dann richtet sich weitgehend alles selber ein, der USB-Stick wurde erkannt, aber nach dem ersten Boot mit der neu geschriebenen SD-Karte dauert die Einrichtung noch fast eine halbe Stunde. Allerdings ist alles noch automatisch und man muss nur warten.

Unter http://homassistant:8123/ steht dann eine Weboberfläche zur Verfügung. Mit ein paar Klicks konnte ich dann nach ZigBee-Geräten suchen lassen. Diese müssen sich im Pairing-Modus befinden. Wie das aber bei dem Thermostat geht, dazu lässt sich die Betriebsanleitung nicht aus. In der steht

Die Steuerung und Verbindung mit dem Smartphone sowie der
essentials Smart Home App erfordert zwingend den Einsatz der
essentials Smart Home Zentrale.

Und wenn man die Smart Home Zentrale gekauft hat, dann soll die App die Information raus geben, wie der Thermostat in den Pairing-Modus gebracht wird. Andernfalls darf ich nicht wissen, wie das geht.

ESSENTIALS Heizkörper-Thermostat Zigbee Pairing: oberen Knopf für 4 Sekunden halten

Wenn der Thermostat in den Pairingmodus geht, kommt kurz auf dem LCD das Wort “Pair”. Home Assistant findet dann auch ein Gerät, allerdings weiß HA nicht, was es damit anfangen soll.

Zu dem Thermostat kamen nur 2 Datenpunkte automatisch ins System, RSSI (received signal strength indication) und LQI (link quality indicator), mehr nicht.

So direkt weiß ich nicht weiter. Ich kenne mich auch nicht genug mit ZigBee bzw. home automation aus, um zu beurteilen, wo das eigentliche Problem liegt.

  • Ist der Standard einfach nicht ausreichend, um – z.B. wie bei Bluetooth – alle wichtigen Gerätetypen per Class compliance zu erkennen und anzusprechen?
  • Ist Home Assistant einfach nicht gut genug?
  • Möchten alle Hersteller von Geräten vielleicht möglichst nicht kooperieren?

Ich vermute, dass es letzteres ist. Jeder Hersteller von Home automation Geräten will immer noch zusätzlich seine höchst eigene Zentrale mit verkaufen und den Anwender in sein Ökosystem zwingen. Dann muss das Opfer der Kunde alle zusätzlichen Geräte auch von diesem Ökosystem kaufen.

Nicht nur, dass ich das für sehr kundenunfreundlich halte, ich bin auch der Meinung, dass dadurch viel Elektromüll produziert wird. Weiterhin bin ich der Meinung, dass sich solche Hersteller gerade über längere Sicht selber damit ins Aus manövrieren. Gerade, wenn ich eine größere Installation plane und z.B. alles in meinem Haus automatisierbar machen möchte, muss ich mir Gedanken über Interoperabilität oder Zukunftssicherheit machen. Wird <kleiner Hersteller> noch in 3 Jahren existieren und weitere Geräte / Ersatzteile liefern können? Wird <kleiner Hersteller> Firmwareupdates liefern, wenn Sicherheitslücken bekannt werden? Wird der Cloudservice von <kleiner Hersteller> in 2 Jahren noch am Netz sein? Stehen die Server für die Dienste von <kleiner Hersteller> alle in China?

Bei diesen Fragen muss ich eigentlich zu dem Schluss kommen, dass <kleiner Hersteller> nicht in die engere Wahl kommen kann, und <großer Monopolist> auf dem Papier die sicherere Möglichkeit ist. Im Linux Magazin von 2017 ist das Dilemma schon beschrieben:

Und es gibt weitere gute Gründe, die gegen die fertige Hue Bridge von Philips sprechen: Wer etwa die schlauen Lampen von Ikea (Trådfri) besitzt, kann diese nicht mit dem Hue-Gateway verbinden. Denn die Ikea-Lampen sprechen ZHA, während Hue zwingend ZLL als Zigbee-Profil will. Im schlimmsten Falle muss man also zwei separate Steuergeräte betreiben, die voneinander nichts wissen: Komfortabel geht anders.

Natürlich könnte man verschiedenste Zentralen kaufen und diese dann über Home Assistant (in vielen Fällen zumindest) koordinieren, aber warum sollte ich mehr als eine Zentrale überhaupt haben? Zusätzlich ist es auch nicht so, dass <großer Hersteller> notgedrungen für Qualität steht, besonders schön beschrieben in einem Video von Linus Tech Tips.

Das vorläufige Fazit ist auf jeden Fall, dass es auch heute nicht möglich ist, einfach Heimautomatisierungs-Bestandteile zu kaufen und sie “einfach so” einzusetzen. Mindestens muss man sich lange mit Kompatibilitäten auseinander setzen.

Ich experimentiere jetzt erstmal mit Zigbee2MQTT rum, da ich da zumindest schonmal die Meldungen vom Thermostat roh sehen kann.

Thermische Probleme mit Raspberry Pi 4

Views: 528

Der Raspberry Pi 4 sieht mit seinem Gigabit-Ethernet und den USB-3 Ports genau danach aus, was mir die ganze Zeit fehlte. Einer meine 3er-Pis spielt seit geraumer Zeit mit 3 externen Festplatten NAS. Das natürlich nur unbefriedigend, da mit USB 2 und 100 MBit bei dmcypt-verschlüsselten Platten in guten Fällen 6 MB/s per Samba übertragen werden. Das reicht für vieles, aber längst nicht alles, was ein NAS so tun soll.

Der Pi4 kopiert von dmcrypt zu dmcrypt mit etwa 70 MB/s (über Samba kommt bei mir dann etwa 50 MB/s an – bei großen Dateien), das ist die richtige Richtung. Allerdings lief er beim Kopieren von 2 TB nicht durch, sondern stürzte mit einer Kernel Panic ab. Wenn der als NAS funktionieren soll, muss er auch stabil sein, schnell alleine reicht nicht.

Mit fiel schon beim ersten Starten auf, dass vor allem die USB-Anschlüsse sehr warm werden, die CPU war auch fast nicht mehr anfassbar. Spätestens in einem Gehäuse kann die Wärmeableitung nicht mehr reichen. Bei Tom’s Hardware wird berichtet, dass eine neue Firmware den USB-Controller kühler laufen lassen soll, Tests zeigten 2°C Unterschied, was aber bei weitem noch nicht für den Betrieb in einem Gehäuse ausreicht. Meine Messungen (billigstes Infrarot-Thermometer) des CPU- und RAM-Chips zeigten Oberflächentemperaturen bis 62°C, wenn auf dem Bildschirm das Thermometer erscheint, und auch Thermal Throttling hinweist.

Mit

vcgencmd measure_temp

kann der CPU-Sensor ausgelesen werden. Bei 80°C auf dem Sensor zeigt sich das Thermometer. Meine Versuche bestätigen, dass bei Last nach etwas mehr als 3 Minuten der Prozessor bremst – kopiert der Pi verschlüsselte Dateien, haben alle Chips gut zu tun.

Mit aktiver Kühlung durch einen Lüfter (120mm auf 5 V statt 12 V, so nicht hörbar, und so aufgestellt, dass etwas Luft über die Chips zieht) bleibt der Sensor zwischen 50°C und 60°C, die Oberfläche liegt dann bei akzeptablen 36°C. Im Moment habe ich keine weiteren Abstürze, vielleicht ist die Temperatur das einzige Problem. Auf jeden Fall lasse ich den nur dann als NAS laufen, wenn ich einen Lüfter dran stellen kann, andernfalls ist mir das eine zu heiße Sache.

Der lange Weg zur eigenen RGB-LED-Ansteuerung, Teil 2

Views: 2098

Da der Arduino nicht allzuviel Last an seinen IO-Ports erlaubt, brauchen wir eine Entkopplung, bzw. Verstärkung. Für niedrige Lasten ist das einfach mit einem Transistor als Schaltstufe zu regeln. Ich habe jetzt gerade nach einfachen Erklärungen für diese Schaltung gesucht, aber das klingt alles zu kompliziert. Um die Schaltungszeichnungen zu verstehen, sollte man die Schaltsymbole kennen. Natürlich sind die international nicht genormt, aber meistens stimmen die schon.

In der ersten Version, wo der Controller (hier ein Arduino, kann aber genauso gut ein Raspberry Pi sein) die LED direkt antreibt, sieht das Schaltbild so aus:

Schaltbild Arduino mit LEDD1 ist hier einfach eine LED. Eine LED ist eine Diode, und eine Diode wiederum lässt Strom nur in eine Richtung durch. An der Seite, wo der Strich ist, oder auch: in der Richtung, in die das Dreieck zeigt, muss der Minuspol (bzw. auch Masse) liegen. Einen Transistor anzutreiben ist genauso einfach, wie eine LED. Sehr vereinfacht ausgedrück besteht ein Transistor aus 2 Dioden. So ersetzen wir die LED mit einem Transistor:

Arduino mit Transistorschalter 1

Betrachten wir dabei nur den fett markierten Bereich (und ignorieren erstmal den Widerstand R1), sieht das sehr ähnlich aus. Auch im Transistor ist ein Dreieck, das Richtung Masse zeigt. Der Arduino ist am Anschluss “B” (für Basis) mit seinen 0 (aus) oder 3,3 (an) Volt angeschlossen, der Stromkreis wird dann dadurch geschlossen, dass der Emitter (Anschluss “E”) an der Masse angeschlossen ist.

 

Der Widerstand ist zum Schutz des Controllers und des Transistors sinnvoll. Wenn der Controller den Pin anschaltet, läuft (fast) ungehindert Strom zwischen dem Controller-Pin und der Masse. Der Widerstand ist zur Begrenzung dieses Stroms da.

Nach dem Ohmschen Gesetz ist der zu berechnen. In dieser Erklärung geht es um grobe Berechnungen, um es nicht unnötig kompliziert zu machen (für absolute Genauigkeit gibt es hier auch noch keinen Grund), ignoriere ich ein paar unbdeutende Parameter. Demnach ist U / I = R (U = Spannung in Volt, I = Strom in Ampere, R = Widerstand in Ohm). Die Spannung ist bekannt, das sind U=3,3 Volt, der maximal akzeptable Strom für den Arduino kennen wir auch, der ist I = 40 mA = 0,04 A. Danach ist der minimale Widerstand R = 3,3 V / 0,04 A = 82,5 Ohm. Aber, weniger Strom ist immer besser, also sagen wir mal 10 mA wären nett. Also ist der Widerstand R1 = 3,3 V / 0,01 A = 330 Ohm. Ähnliche Werte, wie z.B. 300 sind hier völlig okay. Wir betreiben den Transistor als Schalter, also an oder aus. Je mehr der Controller und der Transistor geschützt werden können, desto besser, aber ein wenig Strom braucht der Transistor doch noch. Sehr simpel zusammen gefasst: ein Widerstand da ist prima, wenn der zu groß wird, schaltet der Transistor nicht, aber je größer, desto besser.

Natürlich sieht man erstmal nichts, wenn wirklich nur der Transistor vom IO-Pin des Arduino angesteuert wird. Dafür aber ist dann der Rest der Schaltung zuständig. Wenn der Transistor vom Arduino Strom kriegt, schaltet er auf “an”. Dann ist (quasi) die LED direkt an die Stromquelle angeschlossen (wieder nur der fett markierte Bereich):

Arduino mit Transistorschalter 2Der Strom fließt dann – gesteuert durch die Basis – von der Stromversorgung über die LED D1 und den Widerstand R2 durch den Transistor über den Anschluss “C” (Kollektor) aus dem Anschluss “E” (Emitter) zurück zum Minus-Pol der Stromversorgung. Das bedeutet dann übrigens auch, dass man die Stromversorgung für die LED recht frei wählen kann. Insbesondere können mit den 3,3 V des Arduino LED-Module geschaltet werden, die 12 V benötigen.

Auch hier ist wieder ein Widerstand eingezeichnet. Nehmen wir mal an, die Stromversorgung würde bei 12 Volt arbeiten, und wir würden nur eine LED dran betreiben, dann müsste der Widerstand R2 den Strom über die LED begrenzen. Wieder kommt das Ohmsche Gesetz daher, leicht modifziert allerdings. Die Berechnung des Vorwiderstands einer LED erfolgt folgendermaßen (oder durch dieses verlinkte Script): R2 = (US – ULED) / ILED = (12 V – 4 V) / 0,02 A = 400 Ohm (US = 12 V der Stromversorgung, ULED = 4 Volt für weiße LEDs, ILED = 20mA – ist ein Wert, den eigentlich jede LED aushalten können sollte). Realistisch ist auch das wieder eine konservative Schätzung, da wegen des Transistors nicht wirklich 12 Volt von der Stromversorgung einzurechnen sind.

Damit kommen wir dann zu dem Gesamtschaltbild:

Arduino mit Transistorschalter 3Es bleiben noch ein paar Fragen:

  • sind 0,01 A über die Basis okay?
  • wieviele LEDs, bzw. wieviel Last kann betrieben werden?

Diese Fragen werden durch den Typ des Transistors beantwortet. Grundsätzlich ist die Last erstmal darüber begrenzt, welchen Strom der Transistor schalten kann. Ich habe einen BC547C genommen, weil der hier rum lag. Was die Spannungen angeht, liegen wir hier locker in vernünftigen Bereichen. Zwischen Kollektor und Emitter (“C” und “E”) dürfen bis zu 50 Volt liegen (im Beispiel sind es 12 V), zwischen Emitter und Base (“E” und “B”) dürfen 6 Volt liegen (im Beispiel sind es 3,3 V). IC (Strom über dem Kollektor, oder auch “Collector Current Continuous”) darf dauerhaft nicht höher als 100 mA sein, also 5 LEDs.

Die Verstärkung (DC Current Gain) liegt bei der C-Version bei 400. Das wären bei 0,01 A an der Basis also 4 A. Da der Transistor das aber nicht aushält, ist das offensichtlich zu viel (die 4 Ampere werden natürlich nur erreicht, wenn eine entsprechende Last anliegt…). Um bei einer 400fachen Verstärkung auf nur 100 mA zu kommen, sollte der Basisstrom also theoretisch 0,1 A / 400 = 0,00025 A = 0,25 mA sein. Da wir den Transistor aber zum Schalten gerne in vollem Sättigungsbetrieb haben, wird die Basis eigentlich übersteuert (steht unter “Base Emitter Saturation Voltage”) mit 5 mA. Der Maximalstrom über die Basis liegt weit höher, aber wenn wir den Widerstand vor der Basis mit 5 mA berechnen, sind wir auf einem guten Wert. Um den genau zu berechnen nehmen wir jetzt U = UArduino – VBE (sat) = 3,3 V – 0,9 V = 2,4 V. Also R1 = U / I = 2,4 V / 0,005 A = 480 Ohm. Aber alles zwischen 330 Ohm (wie oben behauptet) und 480 Ohm (und auch jeweils ein wenig drüber hinaus) ist an dieser Stelle absolut okay. Wird der Widerstand zu hoch, kommt nicht die ganze Leistung an den LEDs an, wird er zu niedrig, kann der Transistor durchbrennen. Für Experimente ist da genügend Platz, insbesondere, wo diese Transistoren ein paar Cents kosten.

Der lange Weg zur eigenen RGB-LED-Ansteuerung, Teil 1

Views: 364

Wahrscheinlich ist das meiste, was ich lösen möchte und muss, um meine persönliche Ansteuerung für RGB-LED-Strips zu bekommen, schon kompakt irgendwo zusammen gefasst. Ebenso wahrscheinlich gibt es schon alles fertig zu kaufen, ich vermute aber, dass mir dann der Preis nicht gefallen würde. Außerdem: warum einfach, wenn es auch kompliziert geht.

Ich habe ein wenig Grundahnung von elektronischen Schaltungen, aber das war es auch schon. Im Zuge meiner Beleuchtungswünsche kam und kommt einiges hinzu, und wenn jemand von meinen Erfahrungen profitieren kann, dann würde mich das freuen.

Arduino Leonardo

Netterweise hat mir mein Haus- und Hofelektroniker zu Weihnachten einen Arduino Leonardo geschenkt, der für die ersten Versuche hier gute Dienste leisten kann. Bei der Entwicklungsumgebung ist direkt ein Beispiel, das die LED blinken lässt:

// the setup function runs once when you press reset or power the board
void setup() {
  // initialize digital pin 13 as an output.
  pinMode(13, OUTPUT);
}

// the loop function runs over and over again forever
void loop() {
  digitalWrite(13, HIGH);   // turn the LED on (HIGH is the voltage level)
  delay(1000);              // wait for a second
  digitalWrite(13, LOW);    // turn the LED off by making the voltage LOW
  delay(1000);              // wait for a second
}

Pin 13 wurde deshalb gewählt, weil der nicht nur direkt neben GND (Ground, bzw. Masse) liegt, und die LED einfach in die Buchsen gesteckt werden kann, sondern auch, weil die LED “L” auf dem Board damit parallel geschaltet ist.

Arduino Leonardo with green LED

Der Arduino kann eine LED direkt antreiben. Die Pins liefern 3,3 Volt und 40 mA. Für erste Versuche sehr nett, aber ein wenig viel für eine LED. Außerdem sollten die Ausgänge natürlich nicht über Gebühr belastet werden.

CnMemory – USB 3.0 – SATA – Gehäuse: Spaceloop

Views: 330

Bei Pollin gibt es von CnMemory ein Metallgehäuse mit USB 3.0 <-> SATA Interface für 7.95 €. Ich hatte ein paar bestellt, in der Hoffnung, damit aus meinem Raspberry Pi ein NAS zu machen. Bei einer 100MBit-Anbindung des Pis ist natürlich nicht viel Geschwindigkeit zu wollen, das war daher auch nicht der Grund für diesen Kauf. Wie immer möchte ich eigentlich keinen Strom verbauchen, wenn Geräte nicht gebraucht werden, daher sollte die externe Platte auch möglichst immer im Schlafmodus sein. Mein bisheriges USB-Festplatten-Gehäuse verstand den Schlafparameter von hdparm nicht, “hdparm -S 60” wurde nur mit einer Fehlermeldung quittiert.

Deshalb habe ich es mit diesem Interface probiert, einzige Erwartung war, dass die Schlafeinstellung durchgeleitet wird. Und: ich bin mehr als zufrieden. Ja, Schlafen bei Nicht-benutzung funktioniert. Aber mehr noch: das alte Gehäuse machte an der USB-Schnittstelle maximal 21 MByte/s, das von CnMemory immerhin 27 MByte/s (direkt vom Raspberry Pi). Die Netzwerkübertragung stieg von max. 5,5 MBytes/s auf 7.8 MByte/s (mittels Samba).

An meinem Desktop, mit USB-3.0-Schnittstellen, ist quasi kein Unterschied zwischen dem On-Board-SATA-Controller und der externen Festplatte zu sehen. Rohe Geschwindigkeitstests zeigen bis zu 180 MByte/s, Kopieren über Windows liegt bei recht konstanten 130 MByte/s.

Einziger Nachteil ist die Wärmeableitung. Auch wenn das Gehäuse aus Metall besteht, existiert keine direkte Wärmekopplung zwischen Festplatte und Gehäuse. Abstandshalter sind alle aus Plastik. Das ist zwar besser als ein reines Plastikghäuse – so kann wenigstens die Luft über das Metallgehäuse besser gekühlt werden – aber eine direkte metallische Verbindung zwischen Platte und Gehäuse wäre schon besser.

Wie dem auch sei, ich bin sehr zufrieden, insbesondere auch mit dem Preis, alleine das 12 Volt Netzteil kostet einzeln fast soviel.

 

Update 2015-06-08: Neue Revision 1.1 reagiert etwas seltsam.

Brother HL-1110 Treiber unter Linux/CUPS/Raspian

Views: 1162

Da einer meiner Raspberry-Pis anderer Aufgaben wegen im Dauerbetrieb arbeitet, wollte ich ihn auch als Drucker-Spooler nutzen. Einer meiner Drucker ist ein Brother HL-1110, und wie alles, was ich betreibe, soll der nur dann angeschaltet sein, wenn er wirklich benötigt wird, außerdem soll er im gesamten Netzwerk verfügbar sein.

Um den Drucker per CUPS anzusprechen, muss natürlich erstmal CUPS installiert werden. Ich gehe mal davon aus, dass das schon geschehen ist. Um den Brother-Drcker darüberzu nutzen, muss aber auch ein entsprechender Treiber vorhanden sein. Von der Brother-Support-Seite sind DEB- und RPM-Pakete vorhanden, aber keins, dass direkt auf Raspian funktioniert. Aber es gibt die gute Nachricht, dass der Quell-Code verfügbar ist. Und letztlich braucht es für CUPS nur einen kleinen Teil davon.

Ausgepackt gibt es PPD/brother-HL1110-cups-en.ppd, was man CUPS beim Einrichten eines neuen Druckers direkt als File-Upload geben kann. Zusätzlich braucht es noch filter/brother_lpdwrapper_HL1110, das kopiert man auf dem Ziel-System nach /usr/lib/cups/filter kopiert.

Danach ist der HL-1110 über CUPS für jeden Rechner im Netzwerk verfügbar (je nach Sicherheitseinstellungen, natürlich). Vor allem aber kann ich erst den Druckbefehl abschicken, un später den Drucker anmachen.

 

Update 2017-06-25

Da fehlen wohl ein paar Informationen.

Meine Prämisse: ich drucke von Windows aus auf einen Netzwerkdrucker; die Treiber für den Drucker sind unter Windows installiert.

Wird auf den Drucker etwas ausgegeben, hängt offensichtlich die Verarbeitung davon ab, welcher Datentyp geliefert wird. Wird von Linux aus – z.B. die Testseite von der CUPS-Administration – gedruckt, ist der Datentyp “application/vnd.cups-postscript”. Für den wird brother_lpdwrapper_HL1110 als Treiber genommen (definiert in der PPD). CUPS testet, ob diese Datei vorhanden ist, meldet sonst aber einen Fehler.

Wird hingegen von Windows mit den GDI-Treibern gedruckt, ist der Datentyp “application/vnd.cups-raw”, für den kein Filter definiert ist, da er auch nicht benötigt wird. Der Drucker versteht die erzeugte Datei direkt.

Somit ist CUPS hier nur mit der Verwaltung beschäftigt.

Um den Drucker unter Windows zu installieren, kann man den Druckertreiber von Brother runterladen, tatsächlich den Drucker über USB anschließen (das dämliche Installationsprogramm geht davon aus, dass nur lokaler Druck möglich ist…), den Drucker wieder abziehen und an den Raspberry Pi anschließen.
Oder man entpackt das Paket (z.B. Download für Windows 10 64bit) mit 7zip, dann erhält man unter install\driver\gdi\32_64 die Treiber, die man beim Einrichten des Netzwerkdruckers braucht. Dann entfällt das physische Anschließen.

Mit dem “Source”-Paket, das Brother da anbietet kann kein voll funktionierender Druckertreiber gebaut werden. Da fehlen sehr viele Dateien, insbesonder der Source für rawtobr3, ansonsten könnte man sich alles noch aus den verschiedenen RPM- und DEB-Paketen zusammen suchen.