032217_2130_Programming1.png

Programming Pursday – Excel Turnierplan für 5 oder 7 Spieler im Doppel

Aus gegebenem Anlass habe ich einen kleinen Turnierplan in Excel erstellt.

Download Turnierplan_5_7_Spieler.xlsx

Dabei werden 5 oder 7 Spieler jeweils in Doppeln (in unserem Falle FIFA 17 auf der PS4, aber auch für alles andere nutzbar)

Es gibt insgesamt 3 Varianten:

  • 5 Spieler: jeder mit jedem gegen jeden mit jedem, 15 Spiele
  • 7 Spieler: kurze Variante, 7 Spiele, jeder hat 4 Spiele
  • 7 Spieler: lange Variante, 14 Spiele, jeder hat 8 Spiele (hier sind leider 2 Begegnungen doppelt, ich bin aber auf keine anderen Kombinationen gekommen)

Bei 7 Spielern ist die Anzahl der Kombinationen „jeder mit jedem gegen jeden mit jedem“ etwas zu hoch (insgesamt 105 Begegnungen), daher diese beiden Varianten.

Um das Sheet zu benutzen einfach die Namen in die Zellen I2-I6 bzw. I2-I8 eintragen, das Tableau wird automatisch aktualisiert. Nach Eintragen der Ergebnisse wird auch die Tabelle sofort aktualisiert. Es zählen nur Punkte und Tordifferenz.

In den grün markierten Feldern die Namen eintragen
Amazon Dash Button zur Philips Hue Steuerung

Programming Pursday – Amazon Dash Button Hack auf einem QNAP NAS um Phillips Hue zu steuern

Wenn man im Netz auf die Suche geht um einen günstigen Weg zu finden Philips Hue Lichter zu steuern, trifft man unter anderem auf den Amazon Dash Button. Also habe ich mir auch mal einen besorgt, da ich eh gerade neue Geschirrspültabs brauchte. Heute mal als Programming Pursday und nicht als Tinkering Tuesday, da es eigentlich alles rein Software-seitig abläuft und hauptsächlich ein bisschen Skripting in Python ist.

UPDATE 08.10.2017:

Nach einem Firmware Upgrade auf dem NAS (auf Version 4.3.3 oder 4.3.4) funktionierte der Button auf einmal nicht mehr und der Aufruf des Skripts schmiss Fehler. Als ich die fehlenden Programmteile wieder installieren wollte, wurde weder der pip Befehl noch einer der package installer Befehle opkg, ipkg, qpkg in der Shell erkannt. Es half nur eine komplette Neuinstallation, hier in Kurzform:

  • Python (sowohl 2.7 als auch 3) im App Center komplett deinstallieren
  • Entware ng im App Center deinstallieren
  • Reboot des NAS
  • Download der Datei http://entware.zyxmon.org/binaries/other/Entware-ng_0.97.qpkg
  • Entware-ng_0.97.qpkg via manueller Installation im App Center installieren
  • mit SSH auf das NAS verbinden und die folgenden Befehle ausführen:
    opkg update
    opkg upgrade
    opkg install tcpdump
    opkg install python-pip
    pip install –upgrade setuptools
    pip install scapy
    pip install qhue

Dann ist der Start des Skripts, wie unten beschrieben, wieder möglich.

Einrichtung

Erstmal bin ich der normalen Einrichtung über die Amazon Shopping App gefolgt und habe mir die Tabs bestellt um meine 4,99 Euro wieder zu bekommen. Hat super geklappt. Danach geht man wieder in die App und wählt unter „Mein Konto“, „Dash Geräte“, „Geräte verwalten“ seinen Dash Button aus und deaktiviert ihn. Danach kann man ihn direkt neu einrichten (selbes Prozedere), bricht aber im letzten Schritt, wo man ein Produkt auswählen soll mit dem X in der rechten oberen Ecke ab. Die Sicherheitsabfrage bestätigt man und sieht dann in der Geräteverwaltung auch den Warnhinweis:

Warnhinweis bei unvollständiger Einrichtung
Warnhinweis bei unvollständiger Einrichtung

Beim ersten Drücken bekommt man auch in der App einen Hinweis, dass der Button nicht korrekt eingerichtet ist.

Zusätzlich kann man sicherheitshalber noch den Button im Router für die Kommunikation nach außen sperren (z.B. über die Kindersicherung der Fritz.Box).

Dash Button Knopfdruck auf dem QNAP erkennen

Da es im Netz widersprüchliche Anleitungen und diverseste Wege gibt um den Amazon Dash Button für andere Zwecke zu nutzen. Die Anleitung unter https://blog.thesen.eu/aktuellen-dash-button-oder-ariel-etc-von-amazon-jk29lp-mit-dem-raspberry-pi-nutzen-hacken/ ist auf den deutschen Button ausgelegt, also genau richtig für meine Zwecke. Allerdings wird da beschrieben, dass man ein Produkt auswählen muss. Das ist aber nicht richtig, wenn man den Schritten oben folgt.

Um den Dash Button für andere Zwecke zu nutzen braucht man ein Gerät, das ständig den Netzwerkverkehr auf entsprechende Pakete überwacht. Im Fall von Dash sind das UDP-Pakete, die der Button beim Anmelden im WLAN aussendet. In meinem Fall soll das mein QNAP NAS sein, das eh immer an ist und über ein Python Programm. Über das App Center auf dem QNAP hatte ich bereits Python 2.7.3 installiert.

Das Python-Skript in der Anleitung benutzt zusätzliche 2 Pakete:

  • Scapy
  • Tcpdump

Scapy habe ich nicht so einfach installiert bekommen, dazu habe ich erst den Python Installer pip installiert. Eine gute Anleitung hierzu findet man unter http://gleenders.blogspot.de/2014/04/install-python-pip-on-qnap-nas.html. Dabei muss man beachten, dass man in den Befehlen „MD0_DATA“ durch den Namen seines Volumes (in meinem Fall „CACHEDEV1_DATA“) ersetzt.

Danach ganz einfach in der SSH-Konsole den folgenden Befehl ausführen:

pip install scapy

TCPDump ist über entware-ng ganz normal über den Befehl opgk zu installieren (hatte ich schonmal gemacht, Anleitung gibt es unten auf der Seite):

opkg update
opkg install tcpdump

Das Skript aus der Anleitung habe ich in einen separaten Freigabeordner „Skripting/AmazonDash“ gelegt, den ich mir extra dafür angelegt habe. Allerdings gab es beim ersten Drücken des Buttons einen Fehler:

Fehler im Skript
Fehler im Skript

Ein bisschen Recherche im Internet und ich habe das Skript etwas angepasst (ich habe auch den Aufruf von FHEM rausgenommen, da ich das nicht nutze, sondern ja später Philips Hue direkt ansprechen möchte). Hier mein Skript:

from datetime import datetime
import logging
# Constants
timespan_threshhold = 3
# Globals
lastpress = datetime(1970,1,1)
logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
from scapy.all import *
def button_pressed_dash1():
global lastpress
thistime = datetime.now()
timespan = thistime - lastpress
if timespan.total_seconds() > timespan_threshhold:
current_time = datetime.strftime(thistime, '%Y-%m-%d %H:%M:%S')
print 'Dash button pressed at ' + current_time
lastpress = thistime
def udp_filter(pkt):
options = pkt[DHCP].options
for option in options:
if isinstance(option, tuple):
if 'requested_addr' in option:
# we've found the IP address, which means its the second and final UDP request, so we can trigger our action
mac_to_action[pkt.src]()
break
mac_to_action = {'ac:63:be:ac:23:1c' : button_pressed_dash1}
mac_id_list = list(mac_to_action.keys())
print "Waiting for a button press..."
sniff(prn=udp_filter, store=0, filter="udp", lfilter=lambda d: d.src in mac_id_list)
if __name__ == "__main__":
main()

Das funktionierte dann wunderbar:

Erfolgreiche Erkennung des Dash Buttons
Erfolgreiche Erkennung des Dash Buttons

Philips Hue aus Python ansprechen

Nun also der nächste Schritt, Philips Hue ansprechen. Bei mir soll der Button auf dem Nachttisch dazu dienen das Licht an- oder auszuschalten. Also muss ich mir erstmal die Szene besorgen.

Dazu habe ich mich für eine Python Library namens Qhue entschieden. Die machte einen ganz guten Eindruck. Installiert wird die Library mittels

pip install qhue

Dann erstmal das Skript qhue_example.py aus dem GitHub auf das QNAP gebracht, die IP-Adresse meiner Bridge eingefügt und mittels

python ./qhue_example.py

Ausgeführt. Man wird aufgefordert den Button auf der Bridge zu drücken und dann die Entertaste zu betätigen. Hat funktioniert, danach hat man eine Text-Datei im gleichen Ordner mit dem Usernamen für die Bridge, den man braucht. Außerdem werden einem alle Lampen in einem langen String ausgegeben.

Mit ein bisschen rumspielen und z.B. den Befehlen

print bridge.groups()
print bridge.scenes()

Findet man die IDs seiner Gruppen und Szenen. Ich bin an der Gruppe „Schlafzimmer“ und der Szene „SZ Standard“ interessiert. Für die Szene sucht man nach dem Namen und findet so etwas:

u'EMeSZzszX2OssE9': {u'picture': u'', u'locked': True, u'name': u'SZ Standard'

Interessant ist hier die ID ‚EMeSZzszX2OssE9′, die braucht man um die Szene zu aktivieren. Die Gruppe findet man auch, in meinem Fall:

u'7': {u'name': u'Schlafzimmer ', u'lights': [u'3', u'5', u'6'], u'state': {u'any_on': False, u'all_on': False}

Hier interessiert mich die ID 7, man sieht, dass die Lichter mit den IDs 3, 5 und 6 enthalten sind und das Attribut any_on sieht auch schon vielversprechend aus.

Der Plan

Wenn der Dash Button gedrückt wird, soll er erkennen, ob eine Lampe in der Gruppe „Schlafzimmer“ an ist. Wenn das der Fall ist, soll er das Licht im gesamten Schlafzimmer ausschalten, wenn keines an ist, soll die Szene „SZ Standard“ aktiviert werden.

Das Skript für den Dash Button habe ich also wie folgt angepasst:

from datetime import datetime
from os import path
from qhue import Bridge, QhueException, create_new_username
import logging
# Constants
###########
timespan_threshhold = 3
# the IP address of your bridge
BRIDGE_IP = "192.168.178.23"
# the path for the username credentials file
CRED_FILE_PATH = "qhue_username.txt"
# Globals
###########
lastpress = datetime(1970,1,1)
bridge = None
logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
from scapy.all import *
def button_pressed_dash1():
global lastpress
global bridge
thistime = datetime.now()
timespan = thistime - lastpress
if timespan.total_seconds() > timespan_threshhold:
current_time = datetime.strftime(thistime, '%Y-%m-%d %H:%M:%S')
print 'Dash button pressed at ' + current_time
sfind = "uany_on: True"
st = str(bridge.groups[7]())
sgroup = st.replace("'", "")
if sfind in sgroup:
# print sgroup
print "deactivate scene"
bridge.groups[7].action(on=False)
else:
# print sgroup
print "activate scene"
bridge.groups[7].action(scene='EMeSZzszX2OssE9')
lastpress = thistime
def udp_filter(pkt):
options = pkt[DHCP].options
for option in options:
if isinstance(option, tuple):
if 'requested_addr' in option:
# we've found the IP address, which means its the second and final UDP request, so we can trigger our action
mac_to_action[pkt.src]()
break
#Dash Buttons
mac_to_action = {'ac:63:be:96:36:52' : button_pressed_dash1}
mac_id_list = list(mac_to_action.keys())
def main():
# check for a credential file
if not path.exists(CRED_FILE_PATH):
while True:
try:
username = create_new_username(BRIDGE_IP)
break
except QhueException as err:
print "Error occurred while creating a new username: {}".format(err)
# store the username in a credential file
with open(CRED_FILE_PATH, "w") as cred_file:
cred_file.write(username)
else:
with open(CRED_FILE_PATH, "r") as cred_file:
username = cred_file.read()
# create the bridge resource, passing the captured username
global bridge
bridge = Bridge(BRIDGE_IP, username)
print "Waiting for a button press..."
sniff(prn=udp_filter, store=0, filter="udp", lfilter=lambda d: d.src in mac_id_list)
if __name__ == "__main__":
main()

Autorun auf dem QNAP

Startet man das Python-Skript per SSH läuft es standardmäßig erstmal nur so lange, wie die SSH Session auch offen ist. Das soll natürlich nicht so. Daher soll ein Autorun her.

Ich habe das Skript von https://forum.qnapclub.de/thread/33522-ts-231-autorun-sh/ genutzt um das Config-Device des QNAP zu mounten und dann dort wie beschrieben eine autorun.sh anzulegen mit dem folgenden Inhalt:

#!/bin/sh
</span>cd /share/CACHEDEV1_DATA/Skripting/AmazonDash/
python dash_hue.py &

Startet man nun das NAS neu, so sollte das Skript im Hintergrund laufen. Tut es aber leider nicht. Da muss ich nochmal weiter forschen. Bis dahin behelfe ich mir damit das Skript manuell per SSH per screen zu starten:

Skript auf dem QNAP manuell im Hintergrund laufen lassen

screen
cd /share/CACHEDEV1_DATA/Skripting/AmazonDash/
python dash_hue.py

Dann Strg-A und D drücken um die Session in den Hintergrund zu schicken.

Nun kann man sich getrost aus der SSH-Session ausloggen (Putty schließen) und das Skript läuft trotzdem weiter im Hintergrund. Mit dem Befehl screen –r kann man sich auch wieder in die Session einloggen, man muss nur die richtige finden:

Screen Session im Hintergrund
Screen Session im Hintergrund

So kann man dann auch das Log sehen (es wird jeder Button Press auf die Konsole geschrieben) und das Skript beenden.

012716_1748_TinkeringTu1.png

Tinkering Tuesday – Photobooth im Selbstbau Teil 2 – Ablaufsteuerung

Eigentlich passt dieser Post sowohl in den Tinkering Tuesday, als auch in den Programming Pursday, da es hier um den Programmablauf geht. Da der Programming Pursday allerdings erstmal für mein Android-Projekt reserviert sein soll (mit dem ich im Moment nicht so richtig weiterkomme), gibt es den Post als Tinkering Tuesday.

Also, nachdem das grundlegende Setup steht, geht es also an die Steuerung des Ganzen. Da ich mir nicht mit irgendeiner Programmiersprache großen Overhead einbrocken wollte (dazu vielleicht mal mehr, wenn ich meinen Hamstertacho vorstelle, da hab ich alles in Java gemacht und das auf einem Raspberry Pi laufen lassen… war erstmal ein ganz schöner Krampf…) und die meisten Programme, die ich steuern wollte sowieso sehr gut von der Shell zu bedienen sind, hab ich mich für ganz einfache Shell-Skripte entschieden.

Start Photobooth

Das Skript zum Starten hat folgende Schritte:

Hier das Ganze im Skript:

echo stopping mplayer
bash /home/boris/Dokumente/photobooth/shellscripts/kill_mplayer.sh
echo stopping slideshow
bash /home/boris/Dokumente/photobooth/shellscripts/kill_slideshow.sh
echo stopping Eye-Fi Server
bash /home/boris/Dokumente/photobooth/shellscripts/kill_eyefi_server.sh
echo stopping Booth Listener
bash /home/boris/Dokumente/photobooth/shellscripts/kill_booth_listener.sh
echo stopping collage maker
bash /home/boris/Dokumente/photobooth/shellscripts/kill_collage_maker.sh
killall inotifywait

Check Devices

Im ersten Skript, check_devices.sh wird erstmal gecheckt, ob überhaupt alle notwendigen Geräte angeschlossen und unter dem bekannten Namen zu finden sind. Der Arduino sollte unter dem Device /dev/ttyACM0, die TV-Karte unter /dev/video1 zu finden sein. Fehlt der Arduino, wird das Programm abgebrochen, da weder der Schalter, noch der Kameraauslöser funktionieren würden. Die TV-Karte ist optional und führt nur zur Warnung, dass kein Mirroring möglich ist. Den Drucker hab ich nicht extra abgefragt (zum Testen hatte ich ihn meist eh nicht angeschlossen, da ich nicht immer Fotopapier verschwenden wollte).

if find /dev/ttyACM0; then echo Arduino connected; else echo Arduino not connected; exit 1; fi
if find /dev/video1; then echo TV card connected; else echo TV card not connected - no mirroring; fi
exit 0

Serielle Verbindung zum Arduino

Dann wird die Serielle Verbindung zum Arduino mit 9600 Baud hergestellt. Fragt mich bitte nicht mehr, was die anderen Parameter alle sollen, ich bin in dem Fall nur einem Tutorial gefolgt. Das Lesen und Schreiben direkt von der Shell funktioniert bei mir übrigens nicht so richtig, daher ist das später über ein Python-Skript gelöst. Aber dazu später mehr.

Slideshow

Dann wird auch schon die Slideshow gestartet. Da habe ich den Quick Image Viewer QIV genutzt. Der ist sehr flexibel und sehr schnell. Genau richtig für meine Zwecke. Wie man im Skript sieht, wird die Slideshow als Hintergrundprozess gestartet und die Prozess ID (PID) in eine Datei geschrieben. So kann ich später den Prozess ganz einfach wieder beenden.

qiv -f -m -t -i -C -s -S -r /home/boris/Bilder/collagen/  &
echo $! > /home/boris/slideshow_process.pid

Dann kommen die beiden Hauptprozesse, die beide ebenfalls als separate Prozesse gestartet werden.

Collage Maker

Der erste prüft, ob der Collage Maker nicht bereits läuft und startet ihn dann. Dieser überwacht im Grunde nur den Zielordner für die Collagen und wartet auf vier neue Dateien. Sind die angekommen, startet er mit diesen das Skript um die Collage zu erstellen. Da das Programm in einer Endlosschleife läuft, wird sich auch hier die PID gemerkt. So kann ich später mit dem Kill-Skript alles sauber beenden.

Ordnerüberwachung

Hier das Skript für die Ordnerüberwachung, die ich mit inotifywait realisiert hab. Das ist sehr viel effektiver, als ständig den Ordner nach neuen Dateien abzufragen. Man sieht, dass ich noch den Eye-Fi-Ordner nutze. Da war ich zu faul um das zu ändern. Außerdem ist der Eye-Fi Server ebenfalls noch eingerichtet, so kann ich schnell umbauen, falls ich eine andere Kamera nutzen möchte, die Eye-Fi vielleicht besser verarbeitet.

count=1
inotifywait --excludei ".tar" -m --format '%f' -e moved_to -e create /home/boris/Bilder/eyefi | while read line
do
	echo $count
	if [[ $count -eq 4 ]]
		then 
			files=$files,$line
			echo $files
			bash /home/boris/Dokumente/photobooth/shellscripts/make_photocollage.sh $files
			count=$(($count - 3 ))
		else
			if [[ $count -eq 1 ]]
				then
					files=$line
				else
					files=$files,$line
			fi
			echo $files
			count=$(($count + 1 ))

	fi
done

Make Photocollage

Das Skript ist dann dafür zuständig die 4 gefundenen Einzelbilder in eine Photocollage zu verwandeln. Damit das nicht immer gleich und so langweilig aussieht, hab ich 3 verschiedene Layouts erstellt, aus denen mit einem Random-Generator ausgewählt wird. Es ist jeweils das erste Bild in groß dargestellt, die anderen 4 werden in verschiedenen Positionen darum herum positioniert:

collage_t1 collage_t2 collage_t3

Danach werden die vier Bilder zunächst in das passende Format umgewandelt und in einen temp-Ordner gespeichert. Das erste Bild wird auf 666×500 Pixel, die anderen auf 220×165 Pixel verkleinert. Wozu der Crop ist, weiß ich ehrlich gesagt nicht mehr. Mit dem Befehl composite wird dann das entsprechende Bild an die entsprechende Position gesetzt. Dazu habe ich als Ausgangsbasis ein Template im Druckformat für 10×15 bereitgestellt. In meinem Fall habe ich mich für ein einfaches Weiß entschieden. Also kein großes Design-Brimborium drumherum, da wir die entstehenden Photos ja direkt ausdrucken lassen und von den Gästen in ein Album kleben lassen wollten. Dazu lagen dann diverse Stifte bereit, mit denen dann eine Widmung und Deko selbst gemalt werden konnte.

Das fertige Bild wurde dann zum Drucker geschickt (mit dem lp-Befehl) und nochmal in ein anderes weißes Template mittig eingebunden, da der Monitor ein anderes Seitenverhältnis hat. So wurde oben und unten nochmal weiß aufgefüllt.

Im letzten Schritt wird die Slideshow neu gestartet, damit auch das neue Bild mit in die Rotation aufgenommen wird.

arr=$(echo $1 | tr "," "\n")
count=1

rnd=$(($RANDOM%3+1))
echo Vorlage $rnd
if [[ rnd -eq 1 ]]
then
	posImg1="+20+20"
	posImg2="+660+230"
	posImg3="+420+415"
	posImg4="+660+415"
elif [[ rnd -eq 2 ]]
then
	posImg1="+20+50"
	posImg2="+660+20"
	posImg3="+660+218"
	posImg4="+660+415"
elif [[ rnd -eq 3 ]]
then
	posImg1="+117+20"
	posImg2="+20+415"
	posImg3="+340+415"
	posImg4="+660+415"
fi


for x in $arr
do
	if [[ $count -eq 1 ]]
	then 
		first=$x
		convert -gravity Center -crop 613x460+1+0 -resize 666x500  /home/boris/Bilder/eyefi/$x /home/boris/Bilder/temp/$x.png
		composite -geometry  $posImg1 /home/boris/Bilder/temp/$x.png /home/boris/Vorlagen/template_600.jpg /home/boris/Bilder/temp/collage_$first
		count=$(($count + 1 ))
	elif [[ $count -eq 2 ]]
	then 
		convert -gravity Center -crop 613x460+1+0 -resize 220x165 /home/boris/Bilder/eyefi/$x /home/boris/Bilder/temp/$x.png
		composite -geometry $posImg2 /home/boris/Bilder/temp/$x.png /home/boris/Bilder/temp/collage_$first /home/boris/Bilder/temp/collage_$first
		count=$(($count + 1 ))
	elif [[ $count -eq 3 ]]
	then 
		convert -gravity Center -crop 613x460+1+0 -resize 220x165 /home/boris/Bilder/eyefi/$x /home/boris/Bilder/temp/$x.png
		composite -geometry $posImg3 /home/boris/Bilder/temp/$x.png /home/boris/Bilder/temp/collage_$first /home/boris/Bilder/temp/collage_$first
		count=$(($count + 1 ))
	elif [[ $count -eq 4 ]]
	then 
		convert -gravity Center -crop 613x460+1+0 -resize 220x165 /home/boris/Bilder/eyefi/$x /home/boris/Bilder/temp/$x.png
		composite -geometry $posImg4 /home/boris/Bilder/temp/$x.png /home/boris/Bilder/temp/collage_$first /home/boris/Bilder/temp/collage_$first
	fi
done
lp /home/boris/Bilder/temp/collage_$first > /home/boris/Dokumente/print.log
composite -geometry +0+60 /home/boris/Bilder/temp/collage_$first /home/boris/Vorlagen/template_720.png /home/boris/Bilder/collagen/collage_$first

if $(kill  $(cat /home/boris/slideshow_process.pid)); then echo Slideshow wird neu gestartet; bash /home/boris/Dokumente/photobooth/shellscripts/start_slideshow_qiv.sh; else echo Slideshow läuft nicht; exit 1; fi

Booth Listener

Der Booth Listener war ursprünglich ein Shell-Skript, welches die Abfrage vom Arduino und die Steuerung selbst übernehmen sollte (siehe auskommentierte Befehle), wurde dann aber durch ein Python-Skript ersetzt. Da war ich wieder faul und hab den Aufruf einfach nur weitergeleitet. Nicht schön, funktioniert aber 😉 Das Skript ruft also letztlich auch nur das Python-Skript im Hintergrund auf und schreibt die PID in eine Datei.

# Kill any existing cat commands logging the terminal
#exec  ps ax | grep "cat /dev/ttyACM0" | grep ? | awk '{system("kill " $1)}'
# Connect the terminal to logging to a file by appending
# cat /dev/ttyACM0 > ardulog &
#cat /dev/ttyACM0 | while read x
#x=$(/home/boris/arduino-serial -b 9600 -p /dev/ttyACM0 -d 2000 -r)
#echo $x
#bash /home/boris/Dokumente/photobooth/shellscripts/start_booth_sequence.sh &
#exit

python /home/boris/Dokumente/photobooth/shellscripts/booth_listener.py &
echo $! > /home/boris/booth_listener_process.pid

Hier dann das dazugehörige Python:

import subprocess
import serial
varStopListeningTemp = 0
try:
	arduino = serial.Serial('/dev/ttyACM0', 9600)
except:
	print "Failed to connect to arduino"
while True:
	var1 = arduino.readline()
	if var1[0] == "1":
		print "Start Booth Sequence"
		subprocess.call("/home/boris/Dokumente/photobooth/shellscripts/start_booth_sequence.sh",shell=True)
		arduino.flushInput()
	else:
		print "Wrong input",var1

Hier wird zunächst die Verbindung getestet. Ansonsten wird auf eine „1“ aus dem Arduino gewartet (die wird übertragen, wenn der Knopf gedrückt wird, siehe Arduino-Sketch). Wird die erkannt, wird über subprocess das Skript start_booth_sequence.sh aufgerufen (nicht im Hintergrund, das heißt das Programm wartet mit der weiteren Ausführung bis das Skript fertig ist. Das verhindert, dass während der Booth-Sequenz ein erneutes Auslösen des Schalters alles durcheinander bringt) und der Input geflusht, sodass auf einen neuen Input gewartet werden kann. Das Ganze auch wieder in einer Endlosschleife.

Booth Sequence

Das Herzstück ist dann die eigentliche Booth Sequenz. Hier kommt es ganz speziell auf das Timing an. Es soll ein Countdown eingeblendet werden, damit man sich erstmal in Position bringen kann, dann muss die Kamera ausgelöst werden. Dann muss im richtigen Moment ein Screenshot des Video-Inputs gemacht werden. Das Ganze vier mal. Hier der komplette Ablauf:

Und hier das dazugehörige Skript:

echo Starting video input
bash /home/boris/Dokumente/photobooth/shellscripts/start_mplayer_composite_input.sh
sleep 2
echo Stopping slideshow...
bash /home/boris/Dokumente/photobooth/shellscripts/kill_slideshow.sh
echo Showing OSD...
bash /home/boris/Dokumente/photobooth/shellscripts/show_osd.sh
echo Trigger cam
python /home/boris/Dokumente/photobooth/shellscripts/trigger_camera.py
sleep 3
xdotool key s
sleep 1
echo Showing OSD...
bash /home/boris/Dokumente/photobooth/shellscripts/show_osd.sh
echo Trigger cam
python /home/boris/Dokumente/photobooth/shellscripts/trigger_camera.py
sleep 3
xdotool key s
sleep 1
echo Showing OSD...
bash /home/boris/Dokumente/photobooth/shellscripts/show_osd.sh
echo Trigger cam
python /home/boris/Dokumente/photobooth/shellscripts/trigger_camera.py
sleep 3
xdotool key s
sleep 1
echo Showing OSD...
bash /home/boris/Dokumente/photobooth/shellscripts/show_osd.sh
echo Trigger cam
python /home/boris/Dokumente/photobooth/shellscripts/trigger_camera.py
sleep 3
xdotool key s
sleep 1
echo Starting slideshow...
bash /home/boris/Dokumente/photobooth/shellscripts/start_slideshow_qiv.sh
sleep 2
echo Stopping video input
bash /home/boris/Dokumente/photobooth/shellscripts/kill_mplayer.sh

Start Video Input

Zuerst wird also der Video-Input gestartet und 2 Sekunden gewartet, damit ich sicher bin, dass dieser funktioniert:

cd /home/boris/Bilder/eyefi
mplayer -fs -vf screenshot -tv driver=v4l2:input=1:device=/dev/video1:norm=NTSC:chanlist=us-bcast:channel=3:alsa=1:adevice=hw.2:audiorate=4800 0:immediatemode=0:amode=1 tv://1 > /dev/null 2>&1 &
echo $! > /home/boris/mplayer_process.pid

Dazu habe ich den mplayer genutzt, der zudem auch gleich eine Screenshot-Funktion mitbringt. Wieder als Hintergrundprozess mit Wegschreiben der PID.

Kill Slideshow

Erst dann wird die Slideshow gekillt, damit nicht aus Versehen der Desktop zwischendurch zu sehen ist (sicherheitshalber habe ich diesen aber auch weiß gemacht und alle Icons und die Startleiste ausgeblendet).

kill -TERM $(cat  /home/boris/slideshow_process.pid)

Ein einfacher Kill-Befehl. In einem separaten Skript, da ich das des Öfteren wiederverwende.

On Screen Display

Hier habe ich zwei Varianten getestet. osd_cat war leider nicht wirklich zuverlässig, was die Anzeige anging, daher habe ich versucht mit sleep-Befehlen einigermaßen das Timing zu bekommen. War aber auch nicht zufriedenstellend. Darum habe ich mich nach Alternativen umgesehen und bin auf aosd_cat gestoßen und das funktioniert performant und sehr gut. Lediglich mit der Skalierung der Schrift hatte ich etwas zu kämpfen. Hier braucht man einen installierten Font, der auf die Größe skaliert werden kann. Am besten bin ich dann mit „bitstream bold 100“ gefahren. Der Countdown wird am oberen Bildrand (da ist der Weg zur Kameralinse kürzer) in rot angezeigt.

echo "5" | aosd_cat -R red --font="bitstream bold 100" --x-offset=620 --y-offset=-820 --transparency=2 --fade-full=500
echo "4" | aosd_cat -R red --font="bitstream bold 100" --x-offset=620 --y-offset=-820 --transparency=2 --fade-full=500
echo "3" | aosd_cat -R red --font="bitstream bold 100" --x-offset=620 --y-offset=-820 --transparency=2 --fade-full=500
echo "2" | aosd_cat -R red --font="bitstream bold 100" --x-offset=620 --y-offset=-820 --transparency=2 --fade-full=500
echo "1" | aosd_cat -R red --font="bitstream bold 100" --x-offset=620 --y-offset=-820 --transparency=2 --fade-full=500

Der Vollständigkeit halber hier mein Skript für osd_cat:

echo "5" | osd_cat --pos=top --color=red --delay=1 --age=0 --align=left --font="-*-clean-*-*-*-*-175-*-*-*-*-*-*-*" --shadow=5 --shadowcolour=black &
sleep 0.8
echo "4" | osd_cat --pos=top --color=red --delay=1 --age=0 --align=left --font="-*-clean-*-*-*-*-175-*-*-*-*-*-*-*" --shadow=5 --shadowcolour=black &
sleep 0.8
echo "3" | osd_cat --pos=top --color=red --delay=1 --age=0 --align=left --font="-*-clean-*-*-*-*-175-*-*-*-*-*-*-*" --shadow=5 --shadowcolour=black &
sleep 0.8
echo "2" | osd_cat --pos=top --color=red --delay=1 --age=0 --align=left --font="-*-clean-*-*-*-*-175-*-*-*-*-*-*-*" --shadow=5 --shadowcolour=black &
sleep 0.8
echo "1" | osd_cat --pos=top --color=red --delay=1 --age=0 --align=left --font="-*-clean-*-*-*-*-175-*-*-*-*-*-*-*" --shadow=5 --shadowcolour=black &

Kamera auslösen

Danach wird dann die Kamera ausgelöst. Da ich wieder mit dem Arduino kommunizieren muss, wieder über ein Python-Skript:

import serial

try:
	arduino = serial.Serial('/dev/ttyACM0', 9600)
except:
	print "Failed to connect to arduino"
arduino.write('1')

Auch recht einfach gehalten, es wird lediglich eine „1“ an den Arduino gesendet (der wartet darauf und löst dann die Kamera aus, siehe Arduino-Sketch).

Auf dem Arduino läuft folgendes Sketch:

#include <Metro.h>

int PIN_CAMERA_SONY = 2;
int PIN_CAMERA_PANASONIC = 8;

int buttonPin = 11;
int buttonState = 0;
unsigned long lastMeasureTimeButton = 0;
unsigned long measureTimeButton = 0;

unsigned long debounceDelay = 200;    // the debounce time
int cameraPin = PIN_CAMERA_PANASONIC;


void setup() 
{ 
  
  pinMode(buttonPin, INPUT);           // set pin to input
  digitalWrite(buttonPin, HIGH);       // turn on pullup resistors
  Serial.begin(9600);
  
  pinMode(cameraPin, OUTPUT);
  digitalWrite(cameraPin, LOW);
} 
 
 
void loop() 
{ 
  // Eingabe von PC erwartet
  if ( Serial.available()) // Prüfen, ob mindestens ein Zeichen vorhanden ist
  {
    char ch = Serial.read();
    if( isDigit(ch) ) // ASCII-Zeichen zwischen 0 und 9?
    {
       switch (ch) {
        case '0':    
          //
          break;
        case '1':    
          // trigger camera
          
          //Serial.println("trigger");
          digitalWrite(cameraPin, HIGH);
          delay(2000);
          digitalWrite(cameraPin, LOW);
          break;
        case 2:    
          //
          break;
        case 3:    
          //
          break;
        case 4:    
          //
          break;
        default:
            //Serial.println("default");
          ;//do nothing
       } 
    }
  }
  
  // Eingabe über Schalter erwartet
  int readingButton = digitalRead(buttonPin);
  if(readingButton != buttonState) {
    buttonState=readingButton;
    if(buttonState==LOW) {
      measureTimeButton=millis();
      if( (measureTimeButton - lastMeasureTimeButton) > debounceDelay ) {
          Serial.println("1"); //Button pushed debounced
      }
      lastMeasureTimeButton = millis();
    }
  }
}

Im Setup werden die Input-/Output-Pins bereit gemacht und die serielle Verbindung auf 9600 Baud eingestellt.

Im Loop wird dann geprüft, ob ein serieller Input verfügbar ist. Ist dem so, wird es eingelesen und geprüft um was es sich handelt. Wie schon beschrieben wird auf eine „1“ gewartet und dann der Kamera-Pin für kurze Zeit auf High gesetzt. Weiterhin wird der Button überwacht. Hier ist noch ein debouncing eingebaut. Wenn das alles normal funktioniert hat, wird eine „1“ an den PC gesendet.

Hier noch das Skript mit dem ich es über die Shell versucht habe (funktioniert glaube ich genau einmal, dann nicht mehr. Hatte glaube ich was mit dem Flush zu tun oder so…):

/home/boris/arduino-serial -b 9600 -p /dev/ttyACM0 -d 2000 -s 1

Screenshot

Mit dem Tool xdotool kann man ganz einfach Tastatureingaben simulieren. Da der mplayer mit der Taste „s“ einen Screenshot macht, wird einfach im richtigen Abstand zur Auslösung die Taste „s“ gesendet.

Ende der Sequenz

Am Ende wird einfach wieder die Slideshow mit dem o.g. Skript start_slideshow_qiv.sh gestartet (hier ist das neue Bild noch nicht erstellt, das passiert parallel im Hintergrund und nach erfolgter Erstellung wird ja die Slideshow neugestartet, wie oben beschrieben) und danach der Video-Input wieder beendet:

echo Shutting down mplayer...
if $(kill  $(cat /home/boris/mplayer_process.pid)); then echo Shutdown successful; exit 0; else echo Shutdown failed; exit 1; fi

Stop Photobooth

Das Stop-Skript führt im Grunde nur alle Kill-Skripte aus und beendet dann zur Sicherheit auch noch alle inotifywait-Prozesse. Die Skripte für die Slideshow und den MPlayer sind ja schon bekannt, hier also nur noch der Rest:

echo stopping mplayer
bash /home/boris/Dokumente/photobooth/shellscripts/kill_mplayer.sh
echo stopping slideshow
bash /home/boris/Dokumente/photobooth/shellscripts/kill_slideshow.sh
echo stopping Eye-Fi Server
bash /home/boris/Dokumente/photobooth/shellscripts/kill_eyefi_server.sh
echo stopping Booth Listener
bash /home/boris/Dokumente/photobooth/shellscripts/kill_booth_listener.sh
echo stopping collage maker
bash /home/boris/Dokumente/photobooth/shellscripts/kill_collage_maker.sh
killall inotifywait
#if $(ps ax | pgrep -f start_booth_listener > /home/boris/booth_listener.pid); then echo Booth Listener is running; else echo Booth Listener is not running; exit 0; fi
#echo Shutting down...
#if $(kill  $(cat /home/boris/booth_listener.pid)); then echo Shutdown successful; exit 0; else echo Shutdown failed; exit 1; fi
echo Shutting down booth listener...
if $(kill  $(cat /home/boris/booth_listener_process.pid)); then echo Shutdown successful; exit 0; else echo Shutdown failed; exit 1; fi
if $(ps ax | pgrep -f start_collage_maker > /home/boris/collage_maker.pid); then echo collage_maker is running; else echo collage_maker is not running; exit 0; fi
echo Shutting down...
if $(kill  $(cat /home/boris/collage_maker.pid)); then echo Shutdown successful; exit 0; else echo Shutdown failed; exit 1; fi

Eye-Fi

Da ich mit Eye-Fi auch schon recht weit gekommen bin, bis mich das Abstürzen der Lumix aufgehalten hat, will ich die entsprechenden Skripte nicht für mich behalten:

if $(ps ax | pgrep -f rifec > /home/boris/eyefiserver/rifec-master/rifec.pid); then echo Eye-Fi Server already running; exit 0; else echo Eye-Fi Server is not running; fi
if $(/home/boris/eyefiserver/rifec-master/rifec.pl --config=/home/boris/eyefiserver/rifec-master/rifec.config -d); then exit 0; else exit 1; fi

Hier wird nur geprüft ob der Eye-Fi Server bereits läuft (wie im vorherigen Post schon beschrieben benutze ich hier Rifec), ansonsten wird er gestartet.

Und auch das Kill-Skript ist recht einfach gehalten:

if $(ps ax | pgrep -f rifec > /home/boris/eyefiserver/rifec-master/rifec.pid); then echo Eye-Fi Server is running; else echo Eye-Fi Server is not running; exit 0; fi
echo Shutting down...
if $(kill  $(cat /home/boris/eyefiserver/rifec-master/rifec.pid)); then echo Shutdown successful; exit 0; else echo Shutdown failed; exit 1; fi

Fazit

Wie man sieht, habe ich viel in eigene Skripte gekapselt und mich um Flexibilität und Wiederverwendbarkeit bemüht. Allerdings wird mein Setup wohl so ein Sonderfall sein, dass damit wohl eh niemand anderes direkt 1:1 etwas anfangen kann 😉 Aber vielleicht gibt es ja den ein oder anderen Denkanstoß.

Im nächsten Teil gibt es dann wieder weniger technisches mit dem dritten Teil, dem Bau des eigentlichen Kastens. Je nachdem wie lang der wird, wird dann das fertige Produkt in einem weiteren Teil detaillierter vorgestellt.

Programming Pursday – Parse.com schließt die Tore

Nur ein kurzer Post für heute. Parse.com, der Backend as a Service Anbieter von Facebook wird zum 28.1.2017 den Betrieb vollständig herunterfahren. Das ist an dieser Stelle deswegen interessant, weil ich den Service ursprünglich für mein Projekt Routineer ausgesucht hatte.
Spannend aber auch, weil gleichzeitig das Release des Open Source Parse Servers angekündigt wurde, der an sofort bereit steht. Alles in Node.js.

Alles weitere, auch ein Tool um eine bestehende Umgebung in die eigene Datenbank zu migrieren gibt es hier: http://blog.parse.com/announcements/introducing-parse-server-and-the-database-migration-tool/

Programming Pursday – greenDAO Tutorial für Beginner

Entgegen meines vorherigen Blogposts habe ich mich vorerst doch gegen ActiveAndroid und für greenDAO entschieden. Einen genauen Grund kann ich nicht nennen, diverse Kommentare über ActiveAndroid und seine Schwächen haben mich wohl dazu bewogen. Ob greenDAO da besser abschneidet kann ich nicht sagen, aber ich versuche es einfach mal.

Die Dokumentation von greenDAO finde ich für Anfänger nicht sonderlich leicht, daher dokumentiere ich meine Schritte hier einmal. Ausgangspunkt sind die offizielle Dokumentation, die github Readme und ein weiteres deutsches Tutorial, welches allerdings ohne online-Abhängigkeiten arbeitet. Das möchte ich aus Vereinfachungsgründen eigentlich nicht.

Nachdem also Android Studio und das Android SDK nach ein paar Wochen Pause erst einmal wieder ca. zwei Stunden für das Update und das nachfolgende Korrigieren des Versionschaos gebraucht haben (geht nur mir das so, dass nach jedem Update irgendwas immer nicht funktioniert?), lege ich mit meinem Routineer-Projekt auf Basis von ListViews und direktem SQLite Zugriff los.

EDIT: Leider funktioniert die Code-Formatierung nicht so zuverlässig… ich versuche das nochmal zu korrigieren…

Generator erstellen

Als ersten Schritt muss man sein Datenmodell definieren. Das sollte man natürlich schon vorher auf einem Blatt Papier oder dem Programm seiner Wahl gemacht haben. Damit greenDAO nun damit umgehen kann, muss eine eigene Applikation dafür gebaut werden, der DaoGenerator. Das machen wir, indem wir unserem Android-Projekt ein neues Modul hinzufügen, in meinem Fall heißt es RoutineerDaoGenerator. Rechtsklick auf den Projektnamen, New, Module auswählen.

Daraufhin ein Modul vom Type „Java Library“ erstellen:

Meine Einstellungen sehen wie folgt aus:

Nachdem wir uns unter dem Link http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22de.greenrobot%22%20AND%20a%3A%22greendao-generator%22 die aktuelle Versionsnummer geholt haben (zum Zeitpunkt des Schreibens 2.1.0) öffnen wir die Datei build.gradle und editieren diese, sodass sie wie folgt aussieht:

apply plugin: ‚application‘
apply plugin: ‚java‘
apply plugin: ‚maven‘

mainClassName = „de.routineer.routineer.RoutineerDaoGenerator“

repositories {
mavenCentral()
}

dependencies {
compile fileTree(dir: ‚libs‘, include: [‚*.jar‘])
compile ‚de.greenrobot:greendao-generator:2.1.0‘
}

Datenmodell definieren

Jetzt können wir in der Klasse RoutineerDaoGenerator das Datenmodell definieren, das für dieses Tutorial aus einer TaskList und einem Task besteht:

Zuerst wird ein Schema definiert. Hierzu wird die Datenbankversion (wir beginnen mit 1) und ein Packagename benötigt. Hier nutze ich das gleiche Package wie das des Android-Projekts. Ich gebe aber noch den Zusatz „model“ an, sodass sich folgendes ergibt:

Schema schema = new Schema(1, "de.routineer.routineer.model");

Meine Entities und deren Beziehungen sehen erstmal wie folgt aus:

//Entität (Tabelle) Tasklist definieren
Entity tasklist = schema.addEntity("Tasklist");
tasklist.addIdProperty();
tasklist.addStringProperty("name");

//Entität (Tabelle) Task definieren
Entity task = schema.addEntity("Task");
task.addIdProperty();
task.addStringProperty("taskName");
task.addIntProperty("taskIntervalNumber");
task.addIntProperty("taskIntervalTypeID");
task.addDateProperty("taskLastDoneDate");
task.addDateProperty("taskDueDate");

//Beziehung zwischen Tasklist und Task definieren
Property tasklistId = task.addLongProperty("tasklistId").getProperty();
task.addToOne(tasklist, tasklistId);
ToMany tasklistToTask = tasklist.addToMany(task, tasklistId);
tasklistToTask.setName("tasks");

Dann noch die Methode aufrufen um die Dateien zu generieren:

new DaoGenerator().generateAll(schema, "app/src/main/java");

Die Tutorials geben hier den Pfad mit „../“ vorangestellt an, das funktioniert aber bei der Methode nicht. Da wir hier ein anderes Modul in unser Projekt eingefügt haben, müssen wir das Ganze ausgehend vom Projektpfad angeben.

Die Ausführung der Applikation RoutineerDaoGenerator sollte folgendes ergeben:


Und folgende Dateien erzeugen:

Somit ist das Datenmodell fertig.

Nutzung des Datenmodells

Zuerst kommt mal wieder die build.gradle an die Reihe. Nachdem wir unter http://search.maven.org/#search|ga|1|g%3A%22de.greenrobot%22%20AND%20a%3A%22greendao%22 die aktuelle Version gecheckt haben, müssen wir folgende Zeile zur build.gradle der Android app hinzufügen:

Für heute sollen es nur mal kurz die Grundlagen der Nutzung sein, da ich erst noch die Nutzung der alten ListView-Logik anpassen muss. Folgende Attribute der Activity hinzufügen:

private SQLiteDatabase db;
private DaoMaster daoMaster;
private DaoSession daoSession;
private TaskDao taskDao;
private Cursor cursor;

in der onCreate-Methode folgendes aufrufen:

//DAO initialisieren (Datenbanknamen vergeben)
DevOpenHelper helper = new DaoMaster.DevOpenHelper(this, "routineer-db", null);
db = helper.getWritableDatabase();
daoMaster = new DaoMaster(db);
daoSession = daoMaster.newSession();
taskDao = daoSession.getTaskDao();
//Task hinzufügen (der erste Parameter ist die ID, gibt man null mit, wird automatisch eine vergeben, die dann mit getId() abgerufen werden kann)
Task task = new Task(null, "Test", 1, 1, null, null, null);
taskDao.insert(task);
Toast.makeText(this, Long.toString(task.getId()), Toast.LENGTH_SHORT)
        .show();

//Cursor mit allen Tasks abrufen:
cursor = db.query(taskDao.getTablename(), taskDao.getAllColumns(), null, null, null, null, null);

Mit dem Cursor kann man dann z.B. eine ListView mit einem einfach SimpleCursorAdapter füttern. Das soll es aber erstmal für heute gewesen sein.

Programming Pursday – Die große weite Android-Welt

Tja, aus meinem Vorsatz jede Woche einen Blogpost zu schreiben ist mal wieder nichts geworden. Dennoch habe ich mich viel mit Android beschäftigt. Nach der ersten lauffähigen Demo von Routineer, die zugegebenermaßen bisher nur daraus bestand eine Aufgabenliste anzuzeigen und neue Aufgaben in einer Datenbank abzulegen, habe ich gemerkt, dass man an das Thema wohl doch besser nicht so blauäugig rangeht. Vor allem die Speicherung in der Datenbank hat einen enormen Overhead, woraufhin ich mich erstmal mit Alternativen beschäftigen wollte. Mein ursprünglicher Plan sah vor, möglichst eine lokale Datenhaltung zu haben und diese, bei Bedarf mit „der Cloud“ zu synchronisieren. Das ist aber gar nichtmal so trivial, wie man auf Anhieb mal denken möchte.

Deswegen werde ich mich in einem ersten Schritt wohl erstmal darauf beschränken, eine offline Version zu bauen, die aber so ausgelegt sein soll, dass das Speichermodell erweitert oder ausgetauscht werden kann. D.h. erstmal lokal in einer Datenbank, später dann vielleicht online-only um über die Cloud eine Bearbeitung auf mehreren Geräten / von mehreren Benutzern zu ermöglichen.

Aber auch die lokale Datenhaltung will optimiert werden, da mein bisheriger Weg, mit einer Datenbank-Helper-Klasse und eigenmächtigen Datenbankbefehlen (also quasi der Android-Standardweg), sich als recht umständlich erwiesen hat. Die Alternative lautet ORM – Object Relational Mapping. Dies ist ein Architekturpattern, das den Zugriff auf die Datenbanken kapselt und in diesem Falle die Persistierung der POJOs (Plain old Java objects), die ich sowieso anlegen muss, in der Datenbank übernimmt. Simpel gesagt, die Java-Objekte, die ich habe, werden im Hintergrund in die Datenbank geschrieben.

Dabei habe ich mir vor allem greenDAO und Active Android angesehen. Letzteres vor allem, da es auch einige Blogbeiträge gibt, die das Zusammenspiel mit retrofit behandeln. Das wird wohl dann später mein Framework werden um die Cloud-Anbindung zu realisieren. Im Grunde dasselbe Prinzip, Java-Objekte werden an einen Webservice geschickt.

Weiterhin hab ich mich auch noch mit den verschiedenen UI-Elementen auseinandergesetzt. Vor allem damit, die Liste der Aufgaben anzuzeigen. Auch nicht trivial. Da ich mit einer eigenen Datenbankanbindung angefangen hab, war auch der einfachste Weg der Anzeige daraus einen Cursor in eine ListView einzubinden. Das ist aber auch schon längst veraltete Technik, heutzutage benutzt man eine RecyclerView. Die hat aber keinen CursorAdapter, der sich so einfach aus einer Datenbank ziehen lässt. Und dazu gibt es auch noch keine einfachen Tutorials, sondern nur, sagen wir mal, Ansätze und Workarounds, die aber nicht sonderlich performant sein sollen. Mit dem ORM-Ansatz sollte aber auch dieses Problem in den Griff zu bekommen sein, da ich mich nur noch um die Java-Objekte und nicht noch deren Verwaltung in der Datenbank kümmern muss.

Zu guter Letzt ist dann doch noch ein wenig Konzeptarbeit gefragt, womit ich mich derzeit auch immer mal wieder auseinander setze. Das heißt vor allem, wie müssen meine Objekte aussehen, damit ich sie optimal abfragen, sortieren und speichern kann. Aber auch wie ist der App-Workflow und die UI-Gestaltung.

Also, wenn man es gleich richtig machen will ist halt doch etwas mehr Zeit und Vorarbeit notwendig, aber ich hoffe, dass ich nach Weihnachten dann mit der Theorie einigermaßen abgeschlossen habe und dann mit der weiteren Umsetzung fortfahren kann.

2015-10-05 11.58.31

Programming Pursday – Routineer

Die App, die ich schreiben will hat seinen Ursprung in meinen Studien- besser gesagt in meiner WG-Zeit. Damals hatten wir einen Putzplan, den ich in Joomla (Ein CMS, Content Management System: www.joomla.org) implementiert habe. Es gab eine Übersicht aller Aufgaben, die zu erledigen waren inkl. deren Erledigungsintervall. Sobald eine Aufgabe zu erledigen war, wurde sie gelb markiert. War sie überfällig, wurde sie rot. Jede Aufgabe hatte, je nach Zeitaufwand Punkte, die man bekam, wenn man sie erledigte. Pro Tag mussten durchschnittlich 2 Punkte, also etwa 60 Punkte pro Monat erreicht werden. Pluspunkte, konnten in den nächsten Monat übernommen werden, Minuspunkte wurden in Euro in die WG-Kasse gezahlt.

Jetzt wo ich nicht mehr in einer WG sondern mit meiner Frau zusammen wohne, hat natürlich der WG-Kassenaspekt keinen Charme mehr, allerdings wäre die Übersicht sehr hilfreich, welche Aufgabe denn als nächstes ansteht. Außerdem kann man so auch Aufgaben hinterlegen, die man nur ab und zu mal machen muss (Ölstand und Reifendruck beim Auto kontrollieren) oder einfach zu wissen, wann habe ich mein Auto denn das letzte Mal gewaschen. Einige der Dinge sollten selbstverständlich sein, aber mal ehrlich, irgendwas fällt doch immer hinten rüber und das macht man dann doch erst beim nächsten Frühjahrsputz. Oder meine Frau putzt das Bad am Dienstag, ohne dass ich es weiß und weil ich etwas tun will am Mittwoch, mache ich das unwissentlich nochmal (Hm. Okay, das wird nie passieren 😉 )

Ich möchte mir nicht groß Gedanken machen müssen, sondern wie bei allem in unserer heutigen Welt, einfach auf mein Smartphone schauen und das sagt mir, was ich zu erledigen habe. Anders als bei all den anderen ToDo-Listen Apps soll es hier nicht um einmalige Aufgaben, sondern in erster Linie um wiederkehrende Aufgaben gehen. Die Vision ist groß und die Featureliste lang, allerdings werde ich erstmal eine Basis-Version entwickeln, die dem Putzplan von damals ähnlich ist. Dazu habe ich mir die folgenden Features überlegt:

Features Basis-Version

  • Lokale Datenhaltung
  • Ein lokaler Benutzer
  • Aufgabenerstellung
    • Name
    • Intervall
    • Dauer (anstatt Punkte)
  • Aufgabenanzeige
    • Name
    • Letzte Ausführung
    • Fälligkeit
    • Dauer
    • Sortierung: Fälligkeit
  • Aufgabenerledigung
    • Nur sofort

Erweiterte Features

  • Cloud Synchronisation: Wie man in den vorherigen Folgen des Programming Pursdays schon gesehen hat, möchte ich eine Backend-Anbindung, damit man von mehreren Geräten auf seine Aufgaben zugreifen kann (bei mir wären das z.B. das Handy und das Tablet zu Hause)
  • Benutzerregistrierung: Ebenfalls mit dem Cloud-Sync einher geht die Benutzerregistrierung
  • Listen: Standard ist die private Liste, dazu soll es Listen geben, die auch über mehrere Benutzer hinweg geteilt werden können (z.B. WG-Putzplan)
  • Kategorien: Die Aufgaben sollen in mehrere Kategorien aufgeteilt werden (Haushalt, Auto, etc.)
  • Aufgabenerledigung zu einem beliebigen Zeitpunkt (nachträglich)
  • Standardaufgaben: Ich möchte gerne Aufgaben als Standard anbieten, sodass zum einen die Einrichtung leichter fällt (Einrichtungsassistent, z.B. Auswahl „Familienhaushalt“, „WG-Haushalt“, „Auto vorhanden“, etc.), zum anderen aber auch Benchmarks (da schlägt meine Business Intelligence Ausbildung wieder zu) möglich sind („wie oft werden die Aufgaben im Durchschnitt erledigt?“, „bin ich fauler als der Durchschnittsbenutzer?“).
  • Aufgabenbeschreibung: Eigene Notizen und Standardbeschreibungen (z.B. Putz-Empfehlungen, Anleitungen, etc.)

Es gibt noch einige weitere Ideen, aber die behalte ich erstmal für mich, da ich schon nicht sicher bin, ob ich das oben beschriebene überhaupt jemals umgesetzt bekomme 😉 Aber wenn, dann werde ich stinkereich!

Hier noch ein paar Impressionen, wie es dann in der Endversion einmal aussehen könnte (mit der netten App „Wire Flow“ erstellt):

 

 

092415_1626_Programming6.png

Programming Pursday – ParseUI Login und Registrierung

Also, letztes Mal habe ich mit dem Ausführen der App und dem folgenden Fehler geschlossen:

"com.android.dex.DexException: Multiple dex files define Lcom/parse/AbstractQueryController$1;"

Da soll es dann heute weitergehen. Eine Google-recherche deutet daraufhin, dass die angesprochene Klasse wohl in 2 verschiedenen Libraries definiert ist. Da ich ja mit der normalen Parse-Library angefangen hab und dann die ParseUI zusätzlich eingebunden habe (und dabei einiges probiert habe um es build-fähig zu machen), ist das wohl mein erster Ansatz: Build Paths kontrollieren. Das war zu Eclipse-Zeiten kein Problem, aber da ich Android Studio noch nicht so lange kenne, ist das auch erstmal wieder ein Trial & Error.

In den Modul-Settings (Rechtsklick auf den Projektnamen und dann „Open Module Settings“ oder einfach F4) in den Dependencies findet man alles was das Herz begehrt. Da ist einiges doppelt vorhanden, wenn ich das richtig sehe. Also wird eines nach dem anderen entfernt und immer wieder der Build und die Ausführung angestoßen um den Erfolg zu kontrollieren.

Entfernt hab ich {include=Parse-*.jar, dir=libs}, da mir das mit dem Eintrag {include=*.jar, dir=libs} redundant vorkommt. Build ist weiterhin erfolgreich, Ausführung nicht. War zu erwarten. Als nächstes musste der Eintrag com.parse.bolts:bolts-android:1.+ dran glauben, da es im Modul ParseLoginUI den Eintrag com.parse.bolts:bolts-android:1.2.1 bereits gibt. Build erfolgreich, Ausfürhung immer noch nicht.

Doppelt vorhanden sind auch die ParseFacebookUtils. Also auch den manuellen Eintrag aus dem app-Modul gelöscht. Hat wohl erstmal nichts mit dem Fehler zu tun, aber aufräumen ist immer gut.

Als nächstes habe ich die Parse-Library aus meinem lib-Pfad gelöscht. Die ist ja auch schon im ParseLoginUI-Modul vorhanden. Und siehe da: Kein Fehler mehr bei der Ausführung! Leider führte die Ausführung zu folgendem Scrren in meinem Emulator:

Schade. Der Android Monitor gab folgenden Fehler aus:

Unable to start activity ComponentInfo{eu.boriswerner.routineer/eu.boriswerner.routineer.LoginActivity}: java.lang.NullPointerException
(…)
at com.parse.ParseUser.getCurrentUser(ParseUser.java:879)
at eu.boriswerner.routineer.LoginActivity.onStart(LoginActivity.java:61)

Die Methode erstmal auskommentiert und siehe da, ein Login-Screen (man erkennt ganz leicht den Login-Button schimmern).

Da drauf gedrückt kommt auch der eigentliche Screen mit den Credentials und dem Button für den Facebook-Login:

Da drauf gedrückt wird leider wiederum ein Fehler geworfen (java.lang.NoClassDefFoundError: com.parse.ParseFacebookUtils) und die Anwendung beendet. Naja, aber ein Anfang!

Auch Sign Up führt zu einer Registrierungsseite, leider bricht auch diese mit einem Fehler ab (java.lang.IllegalArgumentException: You must register this ParseObject subclass before instantiating it.)

Okay. Aber das ist schonmal ein neuer Anhaltspunkt. Erstmal wird die normale Registrierung gefixt. Facebook kommt später. Das Problem war schnell identifiziert. Ich hatte Parse zwar in meine erste Activity in die onCreate-Methode eingebunden, aber nicht in meine Login-Activity. Folgender Code brachte dann das gewünschte Ergebnis:

Parse.enableLocalDatastore(this);
//Register Parse Classes
ParseObject.registerSubclass(Task.class);
// Initialize Parse API
Parse.initialize(this, "<Your_Key_Here>", "<Your_Key_Here>");
ParseUser.enableAutomaticUser();
ParseACL defaultACL = new ParseACL();
ParseACL.setDefaultACL(defaultACL, true);
			

Registrierung klappt, was auch ein Blick in das Backend bestätigt:

Die oben auskommentierte Methode getCurrentUser sollte dann auch wieder rein, sonst merkt er ja nix vom Login. Na also, wir können uns also registrieren, erfolgreich einloggen und auch wieder ausloggen:

  
 

Die technischen Grundlagen sind also gelegt. Beim nächsten Mal werde ich mich dann wohl eher einmal dem Konzept widmen und näher beschreiben, was die App überhaupt machen soll.

Import module

Programming Pursday – ParseUI einrichten

Im letzten Post schon angekündigt möchte ich Parse nutzen und auch die ParseUI für u.a. den Login. Beim ersten Versuch hab ich mir das Projekt zerschossen, diesmal hab ich ein Backup gemacht und besser aufgepasst.

Der Anleitung folgend erst einmal das Repository kopiert (einfach herunterladen und entpacken, kein Git-Client oder ähnliches notwendig). Danach im Menü über File -> New -> Import Module den Ordner importieren:

Import module  Import module - Source directory

Daraufhin gab es allerdings den Fehler

Error:(4, 0) Cannot get property 'bolts' on extra properties extension as it does not exist

Cannot get property bolts

Dazu hab ich dann das Update auf https://github.com/ParsePlatform/ParseUI-Android/pull/67/files gefunden und die folgenden Zeilen im build.gradle

compile rootProject.ext.bolts  
compile rootProject.ext.androidSupport  
compile rootProject.ext.parse  
provided rootProject.ext.facebookSDK  
provided files("$rootProject.projectDir/ParseLoginUI/libs/ParseFacebookUtilsV4-1.10.0.jar")  
provided files("$rootProject.projectDir/ParseLoginUI/libs/ParseTwitterUtils-1.10.0.jar")
durch diese ersetzt:
compile 'com.parse.bolts:bolts-android:1.2.1'
compile 'com.android.support:support-v4:22.0.0'
compile 'com.parse:parse-android:1.10.1'
provided 'com.facebook.android:facebook-android-sdk:4.0.1'
provided files("$rootProject.projectDir/ParseLoginUI/libs/ParseFacebookUtilsV4-1.10.1.jar")
provided files("$rootProject.projectDir/ParseLoginUI/libs/ParseTwitterUtils-1.10.1.jar")
Dann hat die ParseLoginActivity gemeckert, dass er den Befehl
import com.parse.ParseFacebookUtils;
nicht auflösen kann.
Dazu hab ich dann noch die Library dem Modul ParseLoginUI hinzugefügt. Das hab ich dann auch gleich sicherheitshalber mit den TwitterUtils gemacht:

AddLibrary AddLibraryToModule

Dann ist zumindest der gradle build durchgelaufen.
Trotzdem konnte ParseFacebookUtils noch nicht aufgelöst werden. Was dann letztendlich die Lösung brachte, war die Library ParseFacebookUtils auch dem Modul app hinzuzufügen. Kein Error mehr!
Das ergab den folgenden Eintrag im build.gradle meiner app:
compile files('C:/Users/Boris/Documents/workspace/Routineer/ParseLoginUI/libs/ParseFacebookUtilsV4-1.10.0.jar')
Aus der SampleApp hab ich mir dann noch den Facebook Login abgeschaut:
Zur strings.xml hinzufügen:
     <string name="facebook_app_id">YOUR_FACEBOOK_APP_ID</string>
Zur AndroidManifest.xml hinzufügen:
Im Abschnitt für die ParseLoginActivity:
<meta-data 
android:name="com.parse.ui.ParseLoginActivity.FACEBOOK_LOGIN_ENABLED" 
android:value="true"/>
Darunter dann noch:
<activity android:name="com.facebook.FacebookActivity"
                           android:configChanges=
                               "keyboard|keyboardHidden|screenLayout|screenSize|orientation"
                           android:theme="@android:style/Theme.Translucent.NoTitleBar"
                           android:label="@string/app_name" />
                 <meta-data
                     android:name="com.parse.APPLICATION_ID"
                     android:value="@string/parse_app_id" />
                 <meta-data
                     android:name="com.parse.CLIENT_KEY"
                     android:value="@string/parse_client_key" />
                 <meta-data
                     android:name="com.facebook.sdk.ApplicationId"
                     android:value="@string/facebook_app_id"/>
Um den Facebook Login zu nutzen musste ich erstmal eine Facebook App erzeugen. Das geht über die Seite
Das einzig trickreiche an dem Prozess ist, seinen Developer Hash zu generieren. Das erledigt unter Windows der folgende Aufruf, wenn man sich OpenSSL heruntergeladen hat (Pfad ggf. anpassen):
C:\Program Files\Java\jdk1.8.0_60\bin>keytool -exportcert -alias androiddebugkey -keystore %HOMEPATH%\.android\debug.keystore | "C:\Program Files\OpenSSL\bin\openssl" sha1 -binary | "C:\Program Files\OpenSSL\bin\openssl" base64
Damit Facebook auch weiß, was in meiner App so vor sich geht hab ich erstmal die vorgeschlagenen Tracker aktiviert:
    @Override
    protected void onResume() {
        super.onResume();
    // Logs 'install' and 'app activate' App Events.
        AppEventsLogger.activateApp(this);
    }
    @Override
    protected void onPause() {
        super.onPause();
    // Logs 'app deactivate' App Event.
        AppEventsLogger.deactivateApp(this);
    }
Nun aber zurück ins Android Studio. Da es nun keine Fehler mehr gibt, hab ich dann die SampleProfileActivity aus der SampleApp hergenommen und in meine LoginActivity verwandelt.
Keine Fehler beim Sync, dann also ausführen.
Beim Kompilieren dann aber der Fehler:
"Multiple dex files define Lcom/parse/AbstractQueryController"
Damit geht es dann nächste Woche weiter…
Android Studio Master Detail Flow

Programming Pursday – Auch das beginnt…

Also, Programming Pursday:
Der heißt so, weil ich beschlossen hab einen Tinkering Tuesday einzuführen (siehe Erklärung dort). Dann hab ich mir überlegt, dass ich eigentlich auch gerne wieder mehr programmieren möchte. Donnerstag wäre doch erstmal gut. Aber das ist keine Alliteration, weder im Deutschen, noch im Englischen. Naja, dann wird halt eine daraus gemacht!
Um meinen Fortschritt zu dokumentieren habe ich als erstes Greenshot installiert und so konfiguriert, dass es automatisch die Bilder speichert ohne mich mit irgendwelchen Dialogen zu nerven. Standard ist das aktuelle Fenster. Da wird auch gleich der Dateiname/-pfad in die Zwischenablage kopiert, das ist hilfreich für den Text, den ich nebenbei dazu schreibe. Da kann ich gleich eine Referenz zum Bild einfügen:
Greenshot - Einstellungen
Greenshot – Einstellungen
Mein Bildschirm-Setup umfasst einen 24-Zoll und einen 19-Zoll Widescreen. Das ist ungemein praktisch. Hier mein Programmier-Setup:

Der Hauptbildschirm wird von Android Studio eingenommen.

Bildschirm Setup Hauptbildschirm
Bildschirm Setup Hauptbildschirm

Der Zweitbildschirm hat am linken Rand den Android Emulator, dahinter den Windows Explorer auf diverse Ordner (workspace, APIs, etc.), auf der rechten Seite den Editor um alles zu dokumentieren.

Bildschirm Setup Zweitbildschirm
Bildschirm Setup Zweitbildschirm

Außerdem läuft hier ein Browser (im Moment teste ich noch Edge, da er auf Windows 10 zumindest auf meinem Rechner performanter zu laufen scheint als Firefox. Chrome war ich noch nie ein Fan von…). Manchmal auch auf dem Hauptbildschirm, ist aber ganz praktisch, wenn man einer Anleitung folgt, nicht immer hin- und herschalten zu müssen sondern beides nebeneinander geöffnet zu haben.

 Aber nun zum Programmieren. Ist ja schließlich Programming Pursday!
Ich hab schon länger vor eine App für Android zu schreiben. Ich hab schonmal eine für unsere Bandproben geschrieben um unsere Songs einzugeben und eine zufällige Reihenfolge zu erzeugen, damit wir die Songs nicht immer in derselben Reihenfolge spielen. Das wird ja langweilig. Das hat funktioniert, hab ich allerdings vor ca. 2 Jahren, damals noch mit Eclipse gemacht. Das ist ja alles überholt mittlerweile…
Als erstes hab ich mir deswegen natürlich Android Studio installiert. Nach langem hin und her mit dem mitgelieferten Emulator (ich hab keine Intel CPU, da kann man dann wohl nicht wirklich die ARM Architekturen, sondern nur die x86 Architekturen nutzen) hab ich mich dann für den Genymotion Emulator entschieden. Installiert, gestartet: Läuft! Und das um einiges performanter als der mitgelieferte, der sich beim Starten laaaaaaaange Zeit gelassen hat.
Da ich schon eine ungefähre Idee habe, was für eine App ich entwickeln will, hab ich mich auch gleich um die Cloud-Anbindung gekümmert. Parse.com soll es werden. Das kann man erstmal schön umsonst testen bis zu einer gewissen Größe. Ich weiß noch nicht, ob ich das Ganze auch noch ein wenig kapseln sollte, damit der eventuelle Umstieg auf ein anderes Backend leichter wird… mal sehen.
In Android Studio hab ich dann mit dem Master/Detail Flow gestartet:
Android Studio Master Detail Flow
Android Studio Master Detail Flow
Dann wird erstmal Parse.com integriert. Anleitung gibt es direkt bei Parse und hat auch auf Anhieb funktioniert:
https://www.parse.com/apps/quickstart#parse_data/mobile/android/native/existing
Da ich eine App schreiben will, die im Grunde eine Todo-Liste abbildet und auch offline nutzbar sein soll, fang ich dann doch gleich mal mit dem Parse-Tutorial für die Offline-Todo-app für Android an:
https://www.parse.com/tutorials/using-the-local-datastore
Das setz ich aber natürlich gleich in meine Namenskonventionen und meinen Master/Detail-Flow um. Wird schon schief gehen und wäre ja langweilig 1:1 nachzubauen.

Meine Task-Klasse ist schnell implementiert

Task-Klasse
Task-Klasse
Bei Kapitel 3, dem TodoListAdapter hapert es aber erstmal, da der ParseQueryAdapter mittlerweile Teil der ParseUI ist. Der Anleitung unter
https://github.com/ParsePlatform/ParseUI-Android

ParseUI-Anleitung
ParseUI-Anleitung

folgend, hab ich mir erstmal mein Projekt zerschossen. Parse konnte überhaupt nicht mehr aufgelöst werden, kompilieren ging auch nicht mehr. Da hilft nur neu aufsetzen :-/ Nochmal das Ganze, diesmal wird aber vorher ein Backup gemacht.

Und damit wird der heutige Programming Pursday beschlossen.