Endlich E-Paper!

E-Paper ist eine tolle Technologie - für Bastler nur leider schwer zu bekommen. Die am Markt erhältlichen Module eignen sich entweder nicht für graphische Ausgabe oder sind nur sehr klein. Auf dem Massenmarkt gibt es E-Paper fast ausschließlich proprietär verbaut in Form diverser Ebook-Reader, wie dem Kindle. Diese Geräte sind relativ kostspielig, und die Eignung für Bastelprojekte sieht man so einem Gerät vorher leider nicht an. Um so erfreulicher der Umstand, dass mein Freund Stefan die Tage einen "Kobo mini" Reader im Sonderangebot fand, und ihn zum Chaostreff mitbrachte. Aufgeschraubt, reingeschaut... wow. Einiges da drin sah wie eine Einladung "hack mich" aus - was sich nachher auch bestätigte. Ich musste meinen Eigenen haben!

Die Hardware

Der Kobo mini hat ein 5" großes eInk Touch-Display mit einer Auflösung von 800x600 Pixeln, das 16 Graustufen darstellen kann. Eine 800MHz Freescale CPU (ARMv7), 256 MB Ram, 2GB Flash, 802.11 b/g/n WLAN, und einen Micro USB Anschluss.

Was nicht auf der Packung steht: Die 2 GB Flash befinden sich auf einer Micro-SD-Karte, die innerhalb des Gehäuses in einem Micro-SD-Slot steckt. Auf dieser Karte sind drei Partitionen: boot (ext2), root (ext2) und eine vfat-Partition, auf der die Benutzerdaten liegen. Das System ist ein Linux mit Busybox und liegt ohne weiteren Schutz durch den Bootloader auf dieser SD-Karte. Ausserdem gibt es auf dem Board eine beschriftete(!) UART-Schnittstelle (der Pegel ist 3.3V), die sowohl vom Bootloader, als auch vom Kernel angesteuert wird, und es läuft da sogar ein getty drauf. Der Benutzer root hat kein Passwort gesetzt. Das macht es leicht.

Es gibt im Wesentlichen drei einfache Möglichkeiten, wie man auf den Kobo kommt:

  • Aufschrauben, Header an die UART löten, 115200/8n1, einloggen, fertig.
  • Wlan über die Kobo-Software aktivieren, ausschalten, aufschrauben, SD-Karte entfernen, 2. Partition auf der Karte mounten, Busybox telnetd konfigurieren, unmounten, Karte wieder zurückbauen, einschalten, scannen welche IP er hat, per telnet einloggen, fertig.
  • Oder - ganz ohne aufschrauben - über einen Upgrade-Prozess in der Kobo-Software (die /etc/init.d/rcS liest sich wie ein Handbuch), indem man erst Wlan aktiviert, dann den Kobo über USB ans Notebook anschließt, und im aufgehenden Mass-Storage-Device (Partition 3 auf der SD-Karte) im Verzeichnis .kobo eine Datei KoboRoot.tgz hinterlegt. Der Inhalt dieses Archivs wird beim nächsten Neustart einfach über / entpackt. Sehr pragmatisch. Man kann sich also ein Minimal-File bauen, das einfach den telnetd aktiviert. Das sähe z.B. so aus:

/etc/inetd.conf:

23 stream tcp nowait root /bin/busybox telnetd -i

/etc/init.d/rcMrks:

#!/bin/sh
mkdir -p /dev/pts 
mount -t devpts devpts /dev/pts
/usr/sbin/inetd /etc/inetd.conf

/etc/inittab:

# This is run first except when booting in single-user mode.
::sysinit:/etc/init.d/rcS
::sysinit:/etc/init.d/rcMrks
::respawn:/sbin/getty -L ttymxc0 115200 vt100
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r
::restart:/sbin/init
::respawn:/usr/sbin/inetd -f /etc/inetd.conf.en

Das alles in ein KoboRoot.tgz einpacken, nach /.kobo/ kopieren, neustarten. Mit nmap nach Port 23 scannen, einloggen, fertig.

Die Software

Auf dem Kobo eingeloggt, findet man ein eher spärlich ausgestattetes Linux mit Busybox vor. Es gibt keine bash (sondern Busybox-typisch ash), leider kein Perl, und sonst auch keine aktuelleren1 Hochsprachen. Man könnte sicherlich eigene Tools für ARM crosskompilen und aufspielen, oder eben ein besser ausgestattetes Busybox bauen, aber ich entschied mich aus Trotz nur mit dem auszukommen, was da schon drauf ist, und verwendete ash, awk, cut, und was ich sonst noch so fand.

Die Kobo-Software selbst besteht, soweit ich das nachvollziehen konnte, im Wesentlichen aus drei Binaries: nickel, pickel und hintenburg. nickel und hintenburg sind Prozesse die im Hintergrund laufen, pickel ist ein Konsolentool, das z.B. vom Startscript verwendet wird, um Bilder auf das Display zu kopieren. Das Startscript /etc/init.d/rcS gibt nützliche Hinweise, z.B. wie man WLAN aktiviert.

Das Display erscheint im System als Framebuffer-Device, das man sicher auch manuell ansteuern kann. Der Einfachheit halber bin ich aber bisher beim "pickel"-Tool geblieben.

zcat image.raw.gz | /usr/local/Kobo/pickel showpic

... zeichnet ein Bild. Einige der .raw.gz Files, die z.B. vom Startup-Script verwendet werden, liegen in /etc/images. Das Format der Bilder ist rgb565, die Größe 800x600.

Zur Touchfunktion des Bildschirms fand ich ein Event-werfendes Device /dev/input/event1, ausserdem war eine ältere Version von "evtest" installiert. Etwas kreativer Geräteumgang, und ein Brecheisen später, war es dann auch nutzbar. (siehe unten)

Das Projekt

Ich hätte gerne ein schickes Anzeigeelement für diverse Sensoren, die ich im Laufe der letzten Jahre daheim verbastelt habe. E-Paper eignet sich dafür besser als andere Displaytechnologien, weil es nicht von selbst leuchtet und den Charakter des Raums kaum beeinflusst. Ich mag es nicht, wenn aus allen Ecken irgendwelche Technik mit künstlichem Licht aus Displays und LEDs auf sich aufmerksam macht. E-Paper ist da die perfekte Alternative2.

So sieht es (momentan) aus:

Die Software besteht aus zwei Teilen: Der eine Teil läuft auf dem Kobo, zeigt Bilder an und kümmert sich um das Handling des Touchscreens. Der zweite Teil läuft auf einem Raspberry Pi, der die Daten der Sensoren sammelt. Dieser Teil generiert mit gnuplot und Imagemagick fertige Bilder für den Kobo und stellt sie auf einem internen Webserver bereit. Die Software auf dem Kobo holt sich von dort dann regelmässig Updates.

Auf dem Kobo:

/etc/init.d/rcMrks:

#!/bin/sh
mkdir -p /dev/pts
mount -t devpts devpts /dev/pts
/usr/sbin/inetd /etc/inetd.conf

sleep 6
killall nickel
killall hindenburg
sleep 2

/mnt/onboard/kobo.sh >/dev/null 2>/dev/null &

/mnt/onboard/kobo.sh:

#!/bin/sh

if [ ! -e /mnt/onboard/uuid ]; then
/bin/dbus-uuidgen > /mnt/onboard/uuid
fi

UUID=`cat /mnt/onboard/uuid`

insmod /drivers/ntx508/wifi/sdio_wifi_pwr.ko
insmod /drivers/ntx508/wifi/dhd.ko
sleep 2

ifconfig eth0 up
wlarm_le -i eth0 up
wpa_supplicant -s -i eth0 -c /etc/wpa_supplicant/wpa_supplicant.conf -C /var/run/wpa_supplicant -B
sleep 2

udhcpc -S -i eth0 -s /etc/udhcpc.d/default.script -t15 -T10 -A3 -f -q

fetch () {
    for N in 1 2 3 4 5; do
        wget -q http://raspi.local:8080/kobo/kobo-$UUID-$N.raw.gz -O /tmp/kobo-$N.raw.gz
    done
}

fetch
ln -f /tmp/kobo-1.raw.gz /tmp/kobo.raw.gz

(
while true; do
    script -q -c "evtest /dev/input/event1 2>&1" /dev/null |
    awk '/^Event/ {
        if ($11 > 450) {
            if ($9 < 266)
                exit 2
            else if ($9 > 266 && $9 < 532)
                exit 4
        } else if ($11 > 300) {
            if ($9 < 266)
                exit 1
            else if ($9 > 266 && $9 < 532)
                exit 3
            else
                exit 5
        }
    }'

    ln -f /tmp/kobo-$?.raw.gz /tmp/kobo.raw.gz
    zcat /tmp/kobo.raw.gz | /usr/local/Kobo/pickel showpic 1
    usleep 250000
done;
)&

c=0
while true; do
    battery=`cat /sys/devices/platform/pmic_battery.1/power_supply/mc13892_bat/capacity`
    status=`cat /sys/devices/platform/pmic_battery.1/power_supply/mc13892_bat/status | sed 's/ /%20/g'`
    ip=`ip a | grep 'inet' | cut -d' ' -f6 | cut -d'/' -f 1`
    wget -q -s "http://raspi.local:8080/cgi-bin/kobo.cgi?uuid=$UUID;bat=$battery;ip=$ip;status=$status"

    fetch

    c=$((c+1))

    incr="1"
    if [[ $((c % 5)) = '0' ]]
    then
    incr=""
    fi

    zcat /tmp/kobo.raw.gz | /usr/local/Kobo/pickel showpic $incr
    sleep 60
done

Die selbst gesetzte Challenge war, nur Software zu verwenden, die bereits auf dem Kobo drauf ist. Deshalb wirkt das vielleicht etwas archaisch. Stimmt aber gar nicht. Das ist topmodern! :D Ich erkläre kurz was es tut, von oben nach unten:

Zuerst generiert sich der Kobo eine UUID, weil das ganze Setup später mehrere Geräte unterstützen wird, und jedes Gerät eindeutlich identifizierbar sein muss. Danach läd es die WLAN-Module, konfiguriert das WLAN, und startet einen DHCP-Client um eine IP zu bekommen. fetch() brauchen wir später noch, das holt einmal alle generierten Bilder (1-5) vom Webserver des Raspis. fetch() wird dann auch direkt aufgerufen und ein Hardlink auf von /tmp/kobo.raw.gz auf das erste Bild gesetzt. Dieser Link zeigt immer auf das Bild, das gerade angezeicht wird. Das ist sozusagen der aktuelle Zustand. Danach kommt etwas Code in (...)& - dieser Code wird als eigener Prozess weggeforkt, und läuft parallel zum restlichen Code. Das in den (...)& kümmert sich um die Benutzereingabe am Touchscreen, und der Code drunter sorgt für die regelmässigen Updates.

Das Touchscreen-Handling ist vielleicht etwas erklärungsbedürftig: Die Idee ist, dass die Ausgabe von evtest /dev/input/event1 in awk zu pipen, um damit die Koordinaten zu parsen. Das Problem ist, dass die Version von evtest, die auf dem Kobo drauf ist, die Ausgabe puffert, und sich das auch nicht ausreden lässt. stdbuf aus den Gnu-coreutils war nicht installiert, unbuffer aus dem expect-Paket war nicht installiert, socat ist nicht drauf, aaaaber "script". Wer hätte das gedacht & warum auch immer ausgerechnet _das_ da drauf ist %-). Jedenfalls simluiert mir "script" hier ein Terminal, und das dient dazu, die Ausgabe mit dem Brecheisen zu entpuffern. Awk parsed die Ausgabe, guckt wo gedrückt wurde und beendet sich mit entsprechendem Exit-Code. Das ln eine Zeile drunter, hängt den Hardlink um auf das Bild zum Exitcode, und zeigt es auf dem Display an. Das usleep sorgt dafür, dass da nicht zu schnell dauer-geforkt wird, wenn das Event-Device feuert. Awk bei jedem Klick zu beenden ist ein billiger Weg dafür zu sorgen, dass sich keine Events in der Pipe aufstauen, die sich sonst wie eine Queue verhalten würde.

Der untere Teil sorgt für die regelmässigen Updates und ruft im Wesentlichen einmal pro Minute fetch() auf, um sich die neuen Bilder vom Raspi zu holen. Vorher liefert es noch ein paar eigene Daten beim Raspi ab, die in die generierten Bilder eingebaut werden können, hier jetzt der Batteriestand, der Lade-Zustand, die eigene IP, und die eigene UUID. Das $incr-Konstrukt unten dient dafür, das Display nur einmal alle 5 Minuten vollständig zu refreshen, und ansonsten nur inkrementell Änderungen zu übernehmen. Das zeichnet deutlich schneller, es entsteht aber sog. Ghosting. Deshalb der gelegentliche vollständige Refresh.

Auf dem Raspi:

Ich paste nur das Script, das die Bilder erzeugt. Die Sensordaten sammeln zwei Daemons, die ich in Perl geschrieben habe, und die regelmässig das Bilder-Erzeuge-Script aufrufen. Weil ich gerade so schön dabei war, und mich die ash-Programmiererei auf dem Raspi die bash vermissen hat lassen, hab ich das hier in bash geschrieben.

img.sh:

#!/bin/bash

cd /run/shm

/usr/bin/gnuplot <<END
set terminal png size 800,300
set border linewidth 2

set xdata time
set timefmt "%d.%m.%Y_%H:%M"
set format x "%H:%M"

set grid ytics lt 0 lw 1 lc rgb "#999999"
set grid xtics lt 0 lw 1 lc rgb "#999999"

set offset graph 0, graph 0.03, graph 0.03, graph 0.03
set style line 1 lw 2lc rgb "black"
set key left

set output "plot-1.png"
plot "/home/mrks/temperature.txt" using 2:3 title "Außentemperatur" with lines ls 1

set output "plot-2.png"
plot "/home/mrks/temperature.txt" using 2:4 title "Innentemperatur" with lines ls 1

set logscale y 10

set output "plot-3.png"
plot "/dev/shm/power.txt" using 2:3 title "Leistung A" with lines ls 1

set output "plot-4.png"
plot "/dev/shm/power.txt" using 2:4 title "Leistung B" with lines ls 1

unset logscale y

set output "plot-5.png"
plot "/home/mrks/radiation.txt" using 2:4 title "Dosisleistung" with lines ls 1

END

temp=(`tail -1 /home/mrks/temperature.txt | cut -d' ' -f 3,4`)
temp[0]=${temp[0]%?}
temp[1]=${temp[1]%?}

power=(`tail -1 /dev/shm/power.txt | cut -d' ' -f 3,4`)
rad=`tail -1 /home/mrks/radiation.txt | cut -d' ' -f 3`

mkdir -p /dev/shm/kobo

for uuid in $(ls -1 | grep -E '^kobo-[a-z0-9]{32}' | cut -d'-' -f2); do

    bat=`cat /run/shm/kobo-$uuid/bat`

    convert \( \
            \( -background white -fill black -font Helvetica -pointsize 50 -size 265x133 -gravity center caption:"${temp[0]} °C" \
               -gravity northwest -pointsize 18 -annotate +6+6 "Außentemp." \) \
            \( -background white -fill black -font Helvetica -pointsize 50 -size 266x133 -gravity center caption:"${power[0]} W" \
               -gravity northwest -pointsize 18 -annotate +6+6 "Leistung A" \
               -bordercolor black -border 2x0 \)\
            \( -background white -fill black -font Helvetica -pointsize 50 -size 265x133 -gravity center caption:"$rad &#956;Sv/h" \
               -gravity northwest -pointsize 18 -annotate +6+6 Dosisleistung \) \
            +append -background black -gravity north -splice 0x2 \
            \) \
            \( \
            \( -background white -fill black -font Helvetica -pointsize 50 -size 265x133 -gravity center caption:"${temp[1]} °C" \
               -gravity northwest -pointsize 18 -annotate +6+6 "Innentemp." \) \
            \( -background white -fill black -font Helvetica -pointsize 50 -size 266x133 -gravity center caption:"${power[1]} W" \
               -gravity northwest -pointsize 18 -annotate +6+6 "Leistung B" \
               -bordercolor black -border 2x0 \)\
            \( -background white -fill black -font Helvetica -pointsize 50 -size 265x133 -gravity center caption:"$bat %" \
               -gravity northwest -pointsize 18 -annotate +6+6 "Batterie" \) \
            +append -background black -gravity north -splice 0x2 \
            \) \
            -append -define PNG:color-type=2 bottom.png

    for N in 1 2 3 4 5
    do
        convert \( plot-$N.png -bordercolor white -border 0x15 \) bottom.png -append -define PNG:color-type=2 montage.png

        avconv -v quiet -vcodec png -i montage.png -vcodec rawvideo -f rawvideo -pix_fmt rgb565 montage.raw
        gzip -f montage.raw
        cp montage.raw.gz /dev/shm/kobo/kobo-$uuid-$N.raw.gz
    done
done

Ich lasse das mal so stehen. Ist eigentlich alles straightforward. Oben GNUplot, unten ImageMagick, ganz unten ffmpeg (avconv), um das rgb565 zu erzeugen. Ich lege alle Bilder auf die Ramdisk, weil im Raspi eine SD-Karte steckt, die auf Dauer Schaden nehmen würde, wenn ständig viel geschrieben wird.

Es gäbe noch eine Menge Details zu erzählen, aber das Posting ist sowieso schon etwas länglich. Die nächste Ausbaustufe wird der Multi-Geräte Support. Ist hier schon zum Teil eingebaut, aber noch nicht ganz fertig.