052817_1619_MusicMonday4.jpg

Music Monday – eDrum Trigger Pad to MIDI mit Arduino und Roland PD-8

Heute geht es darum ein eDrum Trigger Pad (das Roland PD-8) MIDI-fähig zu machen. Ich will für ein Projekt, von dem es bald mehr geben wird, einen einfachen und intuitiven Weg um on-the-fly Drumsounds in Cubase einzuspielen. Was kommt da besser als ein Drumpad mit einem Drumstick zu bearbeiten? Das Pad selbst hab ich mir vom Drummer meiner Band http://www.monkeyfly.de/ ausgeliehen. Im Netz ein wenig gesucht und diverse Anleitungen zum kompletten Eigenbau gefunden (wie auch im Artikel der aktuellen make gesehen, mehr dazu aber bei Andy direkt unter http://doktor-andy.de/wordpress//?s=drum).

Die Funktionsweise des Roland PD-8 ist prinzipiell genauso, also ein Piezo, der angeschlagen eine Spannung abgibt. Das lässt sich genauso mit dem Arduino auslesen und dann per MIDI weitergeben.

Da das auch mein erster MIDI Gehversuch mit dem Arduino ist, hab ich mir erstmal die grundlegende Funktionsweise von seriellem MIDI (ich hatte keine Lust und keine Teile mir ein richtiges MIDI-Gerät zu bauen, ist ja erstmal nur zum Testen). Dazu braucht es eigentlich nur einen Arduino, der per USB angeschlossen wird, hairless MIDI (Seriell zu MIDI Konverter) und ein virtuelles MIDI Device, für Windows ist hier wohl loopMIDI die beste Wahl.

Nach der Installation von loopMIDI einfach mit dem + einen Port hinzufügen.

hairless MIDI muss nichtmal installiert werden, sondern nur gestartet. Als seriellen Port wählt man dann einfach seinen Arduino aus (COM-Port bekommt man aus der Arduino IDE) und wählt im MIDI In/Out den loopMIDI Port aus. Unter Help -> Preferences wählt man noch die Baud-Rate aus, die man im Arduino Programm hinterlegt hat. Ich habe erstmal 9600 genommen.

Einstellungen in hairless MIDI

Wie man dann den Arduino dazu bringt MIDI-fähigen Output auszugeben steht sehr schön auf http://www.instructables.com/id/Send-and-Receive-MIDI-with-Arduino/ beschrieben. Dort in Schritt 5 ist auch der Weg über hairless beschrieben. Das Programm aus Schritt ist sehr schön um das Setup zu testen. Es gibt einfach nacheinander in einer Schleife einige MIDI-Noten aus. Beachtet aber die Baud-Rate dort auf die MIDI-Rate (31250) eingestelt ist. Die muss mit der Rate in den Preferences von hairless übereinstimmen.

Die Noten sollte man dann im Debug Output von hairless sehen (vorher natürlich den Debug aktivieren).

Wenn das funktioniert kann man sich daran machen das Drum-Pad anzuschließen und auszulesen. Das ist eigentlich ganz einfach und wie immer hat auch sparkfun da ein Tutorial für die Grundlagen bereit: https://www.sparkfun.com/tutorials/330

Im Grunde schließt man das Pad einfach an GND und A0 des Arduino an. Dazwischen kommt noch ein 1 MOhm Widerstand und gut ist:

TriggerToMIDI
Anschlussdiagramm (R1 = 1 MOhm)

Das Roland PD-8 hat die Besodnerheit, dass es ein Dual Trigger ist. Das heißt, das Pad und das Rim haben einen eigenen Trigger. Deswegen sollte man ein Klinkenkabel mit TRS Anschluss (Tip-Ring-Sleeve oder einfach Stereo-Klinke) benutzen. Ich schließe aber nur das Hauptpad (Tip) an. Ich habe auch mit beidem experimentiert, aber der Rim-Trigger liefert bei mir nur sehr kleine Ausschläge und das Übersprechen des Hauptpads ist recht groß. Lässt sich sicher auch noch Software-seitig irgendwie lösen.

Wenn die Software von Sparkfun zum seriellen Auslesen funktioniert, kann man sich daran machen alles zu verbinden. Ich habe als Ausgangsbasis den Code von https://beammyselfintothefuture.wordpress.com/2015/01/28/sensing-hit-velocity-and-quick-subsequent-hits-of-a-piezo-with-an-arduino-teensy/ genutzt. Dort habe ich statt der Ausgabe für den Teensy einfach die Ausgabe aus dem MIDI Beispiel genutzt.

Hier gibt es den Source Code:

/*
 * Source www.boriswerner.eu 2017
 * Merged from https://beammyselfintothefuture.wordpress.com/2015/01/28/sensing-hit-velocity-and-quick-subsequent-hits-of-a-piezo-with-an-arduino-teensy/
 * and 
 * MIDI On/Off Messages
 * By Amanda Ghassaei
 * July 2012
 * https://www.instructables.com/id/Send-and-Receive-MIDI-with-Arduino/
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 * 
 */
#define triggerThreshold 10 // If this is set too low, hits on other pads will trigger a "hit" on this pad
#define initialHitReadDuration 500 // In microseconds. Shorter times will mean less latency, but less accuracy. 500 microseconds is nothing, anyway
#define midiVelocityScaleDownAmount 2 // Number of halvings that will be applied to MIDI velocity
#define inputPin A0
 
// Getting the ideal balance of these two constants will ensure that fast subsequent hits are perceived accurately, but false hits are not generated
#define subsequentHitThreshold 1.7
#define subsequentHitThresholdDecaySpeed 14
 
uint16_t highestYet;
uint32_t startReadingTime;
uint32_t highestValueTime;
boolean hitOccurredRecently = false;
 
int noteON = 144;//144 = 10010000 in binary, note on command
int noteOFF = 128;//128 = 10000000 in binary, note off command
int midiNote = 69;//MIDI Note A3

void setup() {
  Serial.begin(9600); //Set Baud Rate to 31250 for MIDI or other rate to use with serial-MIDI-converter
}
 
void loop() {
 
  // Assume the normal hit-threshold
  uint16_t thresholdNow = triggerThreshold;
 
  // But, if a hit occurred very recently, we need to set a higher threshold for triggering another hit, otherwise the dissipating vibrations
  // of the previous hit would trigger another one now
  if (hitOccurredRecently) {
 
      // Work out how high a reading we'd need to see right now in order to conclude that another hit has occurred
      uint16_t currentDynamicThreshold = (highestYet >> ((micros() - highestValueTime) >> subsequentHitThresholdDecaySpeed)) * subsequentHitThreshold;
 
      // If that calculated threshold is now as low as the regular threshold, we can go back to just waiting for a regular, isolated hit
      if (currentDynamicThreshold <= triggerThreshold) hitOccurredRecently = false; // Otherwise, do use this higher threshold else thresholdNow = currentDynamicThreshold; } // Read the piezo uint16_t value = analogRead(inputPin); // If we've breached the threshold, it means we've got a hit! if (value >= thresholdNow) {
    startReadingTime = micros();
    highestYet = 0;
 
    // For the next few milliseconds, look out for the highest "spike" in the reading from the piezo. Its height is representative of the hit's velocity
    do {
      if (value > highestYet) {
        highestYet = value;
        highestValueTime = micros();
      }
      value = analogRead(inputPin);
    } while (timeGreaterOrEqual(startReadingTime + initialHitReadDuration, micros()));
 
    // Send the MIDI note
    //usbMIDI.sendNoteOn(0, (highestYet >> midiVelocityScaleDownAmount) + 1, 1); // We add 1 onto the velocity so that the result is never 0, which would mean the same as a note-off
    MIDImessage(noteON, midiNote, (highestYet >> midiVelocityScaleDownAmount) + 1);//turn note on
    //Serial.println(highestYet); // Send the unscaled velocity value to the serial monitor too, for debugging / fine-tuning
    hitOccurredRecently = true;
  }
}
 
// Compares times without being prone to problems when the micros() counter overflows, every ~70 mins
boolean timeGreaterOrEqual(uint32_t lhs, uint32_t rhs) {
  return (((lhs - rhs) & 2147483648) == 0);
}

void MIDImessage(int command, int MIDInote, int MIDIvelocity) {
  Serial.write(command);//send note on or note off command 
  Serial.write(MIDInote);//send pitch data
  Serial.write(MIDIvelocity);//send velocity data
}

Nach dem Experiment auf dem Breadboard habe ich das Ganze direkt an der Klinkenbuchse zusammengelötet und gut ist.

Anschluss noch über das Breadboard
Und direkt an der Klinkenbuchse zusammengelötet

Hier noch ein kurzes Beispielvideo, dass die Steuerung von DrumMic’a in Cubase mit dem Triggerpad zeigt:

042117_1148_TinkeringTu2.jpg

Tinkering Tuesday – HUEify – JN5168 to Arduino Mega PWM extension

The JN5168 chip itself has only 5 PWM outputs and thus is not able to control more than one RGB LED strip. To overcome this I want to connect an Arduino Mega to the JN5168. The Mega has 15 PWM outputs so up to 5 RGB strips can be connected. To connect the two devices I chose to use the standard UART serial interface that is activated by default in the NXP demos. In this post I will describe how to connect the two devices and what to do to make the Arduino understand commands from the JN5168.

Hardware serial connection from JN5168 to the Arduino

As the Arduino is using 5V level on the serial connection and the JN5168 is using 3.3V a logic level converter (LLC) has to be used. This little device connects the two serial interfaces shifting from 5V to 3.3V. The connection can be seen in the following image (to drive the JN5168 from the Arduino connect an additional wire from the 3V3 pin of the Arduino to PIN 17 of the JN5168):

Connecting the JN5168 to the Arduino Mega via logic level converter
Connecting the JN5168 to the Arduino Mega via logic level converter

Software serial connection from JN5168 to the Arduino (JN5168 program)

I used the default UART port of the JN5168 because of two reasons:

  1. It is activated by default and the demos are already making use of the output
  2. I didn’t connect any additional wires than the ones that need to be connected to flash the chip and use the PWM output and the default UART is also used to flash the firmware

To have a more stable connection to the Arduino I reduced the baud rate from 115600, which is the default rate, to 9600 which the Arduino can handle better. To do this in the file app_start_light.c go to the function vAppMain() and search for the line :

DBG_vUartInit(DBG_E_UART_0, DBG_E_UART_BAUD_RATE_115200);

and change it to:

DBG_vUartInit(DBG_E_UART_0, DBG_E_UART_BAUD_RATE_9600);

Do the same in the function vInitialiseApp() in the same file.

I tried two different approaches to send output to the arduino:

  1. Use the debug output of the TRACE_LIGHT_TASK / DEBUG_LIGHT_TASK
  2. Define a custom debug flag to directly output the PWM signal.

As I didn’t have success yet creating multiple RGB endpoints and am waiting for PeeVeeOne to publish his work on multiple endpoints, I first went with option 2 which is currently not able to hand over an identifier for the endpoint which the option 1 is able to. The second option has the advantage that it handles smooth transitions, which the other one doesn’t. Once I am able to create more endpoints I may create another output. I will describe both solutions anyway:

Option 1: TRACE_LIGHT_TASK

First, activate the debug flag in the Makefile:

CFLAGS += -DDEBUG_LIGHT_TASK

Then go to the file app_zcl_light_task.c and search for

DBG_vPrintf(TRACE_LIGHT_TASK

I modified the first appearance (in APP_ZCL_cbEndpointCallback) of the function, where it outputs a new line and the RGBL (Red, Green, Blue, Level) values to include an identifiying symbol (§) and the endpoint:

DBG_vPrintf(TRACE_LIGHT_TASK, "\n§ EP %d R %d G %d B %d L %d ",
psEvent->u8EndPoint, u8Red, u8Green, u8Blue, sLight.sLevelControlServerCluster.u8CurrentLevel);

There is a second appearance in the same file and the same function (APP_ZCL_cbEndpointCallback ) where I did the same thing again.

In the file App_Light_ColorLight.c there are two other usages of the function which handle the identification effect. In the function APP_vHandleIdentify() there is currently no handle for the endpoint available so I didn’t modify this one. In the vIdEffectTick() there is an endpoint reference available so I added this similar to the function call above.

DBG_vPrintf(TRACE_LIGHT_TASK, "§ EP %d R %d G %d B %d L %d Hue %d Sat %d\n",
u8Endpoint,
u8Red,
u8Green,
u8Blue,
sLight.sLevelControlServerCluster.u8CurrentLevel,
sLight.sColourControlServerCluster.u8CurrentHue,
sLight.sColourControlServerCluster.u8CurrentSaturation);

In the end the Arduino will receive a message will look like this:

§ EP 10 R 255 G 227 B 139 L 147 Hue 0 Sat 0 X 24939 Y 24701 M 1 On 1 OnTime 0 OffTime 0

This one call will result in 10 PWM changes in the JN5168 to have a nice transition. This is why I currently go with the second option:

Option 2: TRACE_PWM

First, I defined and enabled a new debug flag in the Makefile:

CFLAGS += -DDEBUG_PWM

Then in the file DriverBulb_JN516X_RGB.c I added the following to the definition section at the beginning:

#ifdef DEBUG_PWM
#define TRACE_PWM TRUE
#else
#define TRACE_PWM FALSE
#endif

Then, in the function DriverBulb_vOutput() I added the following after the calls to vAHI_TimerStartRepeat at the end:

#if TRACE_PWM
DBG_vPrintf(TRACE_PWM, "\n# EP %d On %d R %d G %d B %d L %d",
(uint8)10,
bIsOn,
u8Red,
u8Green,
u8Blue,
u8CurrLevel
);
#endif

Together with the first option this will result in the following output (as I said it produces a smooth transition):

§ EP 10 R 255 G 227 B 139 L 147 Hue 0 Sat 0 X 24939 Y 24701 M 1 On 1 OnTime 0 OffTime 0
# EP 10 On 1 R 170 G 151 B 92 L 170
# EP 10 On 1 R 167 G 148 B 91 L 167
# EP 10 On 1 R 165 G 146 B 89 L 165
# EP 10 On 1 R 162 G 144 B 88 L 162
# EP 10 On 1 R 160 G 142 B 87 L 160
# EP 10 On 1 R 157 G 139 B 85 L 157
# EP 10 On 1 R 154 G 137 B 83 L 154
# EP 10 On 1 R 152 G 135 B 82 L 152
# EP 10 On 1 R 149 G 132 B 81 L 149
# EP 10 On 1 R 147 G 130 B 80 L 147

You can also see that the RGB values changed. This is according to the level value. The calculation is:

/* Scale colour for brightness level */
u8Red = (uint8)(((uint32)u8CurrRed * (uint32)u8CurrLevel) / (uint32)255);
u8Green = (uint8)(((uint32)u8CurrGreen * (uint32)u8CurrLevel) / (uint32)255);
u8Blue = (uint8)(((uint32)u8CurrBlue * (uint32)u8CurrLevel) / (uint32)255);

The level is not really necessary in the output because the RGB values are already adjusted. I kept them anyway.

Software serial connection from JN5168 to the Arduino (Arduino program)

For the Arduino part I first setup the serial connection (I connected it to Serial2) and read the input. Based on the first character (§ or # are the relevant ones for the two options) the program decides whether the whole line is relevant.

Then it parses the input line using the function strtok(). Then I set the PWM using analogWrite(pin, value).

The serial input is lagging sometimes (though I think this is only the case when using the serial output to the other serial port) so garbage is getting read as a line. In case of not set values I checked whether the value is correctly set. Otherwise the old value is used. This may lead to wrong values but better than nothing.

My currently used Arduino sketch (using only one PWM output) can be downloaded here:

SerialReadPWMFromJN5168

Conclusion

This may not be the best way of communication (SPI may be better) but it is the easiest to setup. To command 4 separate endpoints I may have to use option 1 and go without transitions or implement them myself on the arduino. The bottle neck seems to be the serial connection as the arduino seems to be too slow to process the serial input fast enough. This is another argument for option 1 as it sends fewer lines. Although it is sending more than one line for a simple light change, too. Maybe the app itself also sends several commands to have a nice transition. I may check this later on. In any case I recommend disabling all other debug outputs to make better use of the serial communication and loose fewer lines.

Currently I am also building a board to connect everything together. It will not be very professional (as I will be using many jumper wires instead of connecting everything directly but I want to be flexible) but should work for me. It will be hidden away anyway. The board connected to the Arduino and the serial connection to the JN5168 can be seen in the upper left with one temporarily connected RGB strip. In the final version they will be connected with screw terminals. The LLC will also go from the breadboard to the custom board.

The completed serial connection from JN5168 to Arduino Mega and custom PWM to RGB LED adapter board
The completed serial connection from JN5168 to Arduino Mega and custom PWM to RGB LED adapter board

This is the current progress with connections ready for two RGB strips.

Custom PWM to RGB LED strip adapter board (work in progress)
Custom PWM to RGB LED strip adapter board (work in progress)

There will be 4 RGB connections in the final version to be connected with screw terminals, the LLC and a connection for an external power source. This is all still work in progress so I will publih the final schematics when I am ready and know that everything is working as expected. And of course I hope that the weather will be getting better here so I can disassemble our night desks and get them to my workbench (actually my balcony which is why I am hoping for good weather 😉

031317_2050_TinkeringTu9.jpg

Tinkering Tuesday – HUEify RGB Strips mit dem NXP JN5168

Wie schon angekündigt soll unsere Nachtkonsolenbeleuchtung mittels eines ZigBee Light Link kompatiblen Chips, dem NXP JN5168, an das Philips Hue System angeschlossen werden. Peter Visser, alias PeeVeeOne (www.peeveeone.com) hat da eine super Vorarbeit geleistet.

Die benötigten Teile sind mittlerweile da. Das ist zum einen der Chip selbst (Amazon: RF MOD, IEEE802.15.4, STD PWR, INT ANT JN5168-001-M00Z By NXP) und dann ein relativ beliebiger FTDI zu USB Serial Converter.

JN5168 anlöten und neue Firmware aufspielen

Da sich meine Lötkünste schon im normalen Bereich, also einem Rastermaß von 2,54mm eher so in Grenzen halten, war ich mit dem Breakout der Pins vom JN5168 maßlos überfordert. Coolerweise war auch mein Lehrmeister in Sachen Elektronik (www.doktor-andy.de) begeistert von dem Projekt, sodass er sofort bereit war mir das anzulöten. Mit einem Schluck Bier und daraus folgender ruhiger Hand am Montagabend wurden dann mit dünnem Kupferdraht die benötigten PINs herausgeführt:

Andy mein Lötmeister! Er sei gepriesen!
Die erste Kupferleitung ist angelötet

Wirklich notwendig für das Projekt sind:

  1. Pin 3 (SPIMISO): der muss auf GND gelegt werden um den Chip programmieren zu können
  2. PIN 17 (VDD): 3,3V (wichtig, am FTDI nicht vergessen von 5V auf 3,3V umzustellen)
  3. PIN 18 (GND): Ground
  4. Pins 19, 20, 21 (DIO 11-13): dies sind die PWM Pins, die für die Ansteuerung des RGB-Strips benötigt werden

Wir haben zusätzlich noch Pin 22, den Reset-Pin, herausgeführt, aber letztlich garnicht benutzt. Kurz stromlos stellen geht auch.

20170306_190156
Kleine Spinnenbeine aus Kupfer

 

Mit blieben dann nur kleine Zuarbeiten, wie etwa das Auftrennen der Streifenrasterplatine in der Mitte:

20170306_190222
HiWi Boris bei der Arbeit

Auf dem Breadboard mit dem FTDI verbunden (Rx an Tx und umgekehrt…):

Die Programmierung ging nach Anleitung von www.peeveeone.com beim ersten Versuch gleich durch. Erst den EEEPROM löschen:

Dann die neue Firmware aufspielen:

Freudig erregt bin ich nach Hause gefahren und hab die Philips Hue App angeschmissen und die Bridge auf die Suche nach dem neuen Lichtlein geschickt. Wichtig ist erst die Suche zu starten, dann den Strom am JN5168 anschließen. Und wer hätte es gedacht: Sofort erkannt!

Angezeigt wird sie als „Color Light 1“

Also auf zum nächsten Schritt, den RGB-Strip ansteuern.

RGB-Strip an Arduino betreiben

Damit ich mir den JN5168 nicht gleich verbrate, weil ich irgendwas nicht korrekt angeschlossen hab, wird erstmal mit dem Arduino die Verkabelung getestet. Dazu nutze ich im Grunde das Anschlussdiagramm von Adafruit aus dem RGB-Strip-Tutorial. Ich habe mich für die Nutzung von MOSFETs entschieden und habe bei Conrad den BUZ11 besorgt. Davon 3 angeschlossen und den Beispiel-Code von Adafruit aufgespielt bringt das gewünschte Ergebnis:

Das einzige was auffällt ist, dass der RGB-Strip nicht korrekt beschriftet ist. Die Pins sind wie folgt gekennzeichnet: + – R – G – B allerdings ist die eigentliche Belegung + – R – B – G. Naja, zum Glück ist wenigstens der + Pin richtig…

Nun gut, dann geht es ans Eingemachte. Die PWM-Pins wurden an den JN5168 angeschlossen, GND entsprechend mit an GND vom Arduino angeschlossen und auch der 3,3V Anschluss vom Arduino an den VDD vom JN5168 angeschlossen. Hier ein Video vom ersten anschalten:

Mehr gab es beim ersten Mal leider nicht zu bewundern, da der Empfang in meinem Arbeitszimmer so schlecht war, dass die meiste Zeit nichts passiert ist. Am nächsten Abend habe ich die Schaltung ins Wohnzimmer rübergebracht. Dabei hatte ich einige Kabel abgemacht um es separat zu tragen. Danach habe ich erstmal 1,5h damit verbracht das Ganze wieder zusammenzusetzen und herauszufinden warum „genau der gleiche Aufbau“ einfach nicht funktioniert. Weder am Arduino noch am JN5168 hat der Strip geleuchtet. Bis ich dann am Abend gemerkt habe, dass ich ein GND-Kabel auf der anderen Seite des Breadboard Siderails angeschlossen habe und nicht bedacht habe, dass die in der Mitte nochmal getrennt sind (im Bild mit rot gekennzeichnet und dann auch mit einem Jumper-Kabel überbrückt)…

Am Wochenende wurde dann alles nochmal richtig getestet und verkabelt und siehe da, es wird Licht und die Farben ändern sich. Allerdings leider nicht ganz so wie erhofft. Grün geht, blau geht, rot irgendwie nicht so richtig und das wichtigste: der Strip lässt sich nicht ausschalten sondern wird dann einfach komplett hell. Spricht eigentlich ja dafür, dass da was mit Common Anode / Common Cathode LED verkehrt läuft, aber eigentlich sollte das alles korrekt sein. Hier mal als Video:

Als erstes habe ich dann den Strip nochmal an den Arduino angeschlossen und geschaut, ob ich bei direkter Ansteuerung jeweils einer Farbe die entsprechenden LEDs zum Leuchten bringen kann. Das funktioniert.

Das erste was ich nun tun werde ist mir nun die Verkabelung am JN5168 nochmal zu Gemüte führen (also mal nur jeweils einen Kanal des Strips anschließen und schauen wie sich die jeweilige LED verhält). Dazu wird dann die Entwicklungsumgebung von NXP installiert und dort etwas geforscht. Das ist da wo der eigentliche Spaß beginnt! Der Doku kann man entnehmen, dass man mehrere Endpoints (also mehrere Lampen) auf einem Chip konfigurieren kann. Außerdem kann man den Chip auch als Aktor (also als Lichtschalter / Dimmer Switch) konfigurieren. Nicht ganz eindeutig ist, ob man auch einen Mischbetrieb fahren kann. Wenn ich richtig lese, dann nicht. Aber das werde ich trotzdem ausprobieren. Aber dafür muss erstmal wieder Zeit her.

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.