Heimautomatisierung für Hardwarebastler (Teil 3)
Funktionen für den Editor
Innerhalb des Editors muss ein Plug-In folgende Aufgaben erfüllen:
- mitteilen, was es kann, wie es angesprochen werden soll und welche Ein-/Ausgänge es besitzt
- Daten zur Verfügung stellen, welche das Layout des Konfigurationsdialoges festlegen
- die vom Konfigurationsdialog zurückgegebenen Daten empfangen
- aus einem Projektfile geladene Daten intern verarbeiten
- die intern gehaltenen Daten so zur Verfügung stellen, dass sie in einem APCP-Projektfile abgespeichert werden können
Für all diese Aufgaben werden wieder Funktionen erwartet, welche vom Plug-In bereitgestellt werden müssen.
static char libname[]="Random Generator"; OAPC_EXT_API char *oapc_get_name(void) { return libname; }
Diese Funktion teilt dem Hauptprogramm mit, wie das Plug-In heißt. Dieser Name wird dann überall dort angezeigt, wo das Plug-In verwendet wird. Wichtig hier: der Name ist in einer globalen Variablen libname
gespeichert, so dass sicher gestellt ist, dass diese Daten auch nach Verlassen der Funktion noch gültig sind.
OAPC_EXT_API unsigned long oapc_get_capabilities(void) { return OAPC_HAS_INPUTS|OAPC_HAS_OUTPUTS| OAPC_HAS_XML_CONFIGURATION|OAPC_ACCEPTS_PLAIN_CONFIGURATION| OAPC_ACCEPTS_IO_CALLBACK| OAPC_FLOWCAT_DATA; }
Diese Funktion liefert verschiedene Flags zurück, welche festlegen, was das Plug-In kann und welche Features es besitzt. Die ersten beiden Konstanten OAPC_HAS_INPUTS
und OAPC_HAS_OUTPUTS
legen fest, dass es sowohl Ein- als auch Ausgänge hat, von diesen hängt dann ab, ob die im folgenden beschriebenen zwei Funktionen vorhanden sein müssen oder nicht. Das Flag OAPC_HAS_XML_CONFIGURATION
ist ein wenig seltsam, zumindest in der aktuellen Softwareversion muss dieses eigentlich immer vorhanden sein. Es legt fest, dass die Konfiguration per XML-Struktur übermittelt wird, Alternativen dazu existieren derzeit keine. Ähnlich scheint es sich mit OAPC_ACCEPTS_PLAIN_CONFIGURATION
zu verhalten, darüber wird festgelegt, wie die im Konfigurationsdialog eingegebenen Daten zurückgeliefert werden. Auch hier ist derzeit nur diese eine Variante vorgesehen. Anders die nächste Konstante OAPC_ACCEPTS_IO_CALLBACK
. Ist diese gesetzt, werden die Ausgänge des Plug-Ins nicht zyklisch abgefragt, vielmehr kann das Plug-In dem OpenPlayer per Callback-Funktion mitteilen, dass neue Daten vorhanden sind, welche von den Ausgängen abgeholt werden können. Dieses Flag ist somit für die Verwendung des Plug-Ins in Player oder Debugger relevant.
Die letzte Konstante legt fest, innerhalb welcher Kategorie das Plug-In im Editor aufgelistet werden soll. Hiermit wurde dem Zufallszahlen-Plug-In die Kategorie »Daten« zugeordnet. Andere Varianten existieren mit OAPC_FLOWCAT_CONVERSION
für »Datenkonvertierung«, OAPC_FLOWCAT_LOGIC
für »Logikoperationen«, OAPC_FLOWCAT_CALC
für »Rechenoperationen«, OAPC_FLOWCAT_FLOW
für »Flusssteuerung«, OAPC_FLOWCAT_IO
für »Eingang/Ausgang« und OAPC_FLOWCAT_MOTION
für die Flow-Elemente-Kategorie »Bewegung«.
Mit den nächsten beiden Funktionen wird der Applikation mitgeteilt, dass die Digitaleingänge 0 und 1 sowie die Ausgänge 0 (digital) und 1 (numerisch) verwendet werden sollen:
OAPC_EXT_API unsigned long oapc_get_input_flags(void) { return OAPC_DIGI_IO0|OAPC_DIGI_IO1; } OAPC_EXT_API unsigned long oapc_get_output_flags(void) { return OAPC_DIGI_IO0|OAPC_NUM_IO1; }
Hier ist besonders darauf zu achten, dass jeder Ausgang nur einmal mit einem entsprechenden Flag für einen Datentyp belegt wird. Eine Angabe wie z.B. OAPC_CHAR_IO7|OAPC_BIN_IO7
wäre unzulässig, da ein Ein- bzw. Ausgang nur exakt einen Datentyp unterstützen und nicht gleichzeitig für Text- und Binärdaten benutzt werden kann.
Die nächste Funktion – bzw. die Daten, welche von ihr erzeugt und zurück gegeben werden – hat es in sich. Hier wird der Applikation mitgeteilt, welches Symbol zur Verwendung im Flow-Editor benutzt werden soll, wie das Layout des Konfigurationsdialoges aussehen soll, und welche Parameter mit welchen Wertebereichen dort angezeigt werden sollen:
OAPC_EXT_API char *oapc_get_config_data(void* instanceData) { struct instData *data; data=(struct instData*)instanceData; sprintf(xmldescr,xmltempl,flowImage,data->config.numRange); return xmldescr; }
Wie zu sehen ist, passiert hier nicht all zu viel, die Informationen selbst stecken in einer XML-Struktur, welche an dieser Stelle zusammengesetzt und zurückgegeben wird. Diese besteht in diesem Beispiel aus drei Teilen: der Defintion des Bildes für den Floweditor, der Definition des Panels für die Parameter sowie der Definition des Hilfe-Panels, in welchem die Ein- und Ausgänge sowie deren Funktion erklärt werden:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <oapc-config> <flowimage>%s</flowimage> <dialogue> <general> <param> <name>numrange</name> <text>Numeric Range</text> <type>integer</type> <default>%d</default> <min>2</min> <max>10000</max> </param> </general> <helppanel> <in0>CLK - generate random digital value</in0> <in1>CLK - generate random numeric value</in1> <out0>RND - random digital value</out0> <out1>RND - random numeric value</out1> </helppanel> </dialogue> </oapc-config>
An Stelle des Platzhalters %s
im XML-Tag <flowimage />
wird das Bild eingefügt. Hier wird ein Base64-kodiertes PNG-Bild in der Größe 106x50 Pixel erwartet. PNG-Bilder lassen sich mit quasi jedem Zeichenprogramm erzeugen, die Base64-Codierung kann beispielsweise online auf www.motobit.com gemacht werden. Das Ergebnis dieser Codierung ist dann ein Textstring, welcher nur noch aus druckbaren Zeichen besteht, die sich als normales Character-Array in die XML-Struktur einfügen.
Mit Hilfe des <general />
-Tags wird eine eigene Tab-Pane erzeugt, in der alle Elemente angeordnet werden, welche sich zwischen diesen befinden. Hier in diesem Beispiel wird ein Eingabefeld (<param></param>
) für Ganzzahlen (<type>integer</type>
) angelegt, welches Werte im Bereich von 2 (<min>2</min>
) bis 10000 (<max>10000</max>
) akzeptiert und mit dem Wert aus data->config.numRange
vorbelegt ist (<default>%d</default>
). An dieser Stelle wird der Wert aus der Variablen numRange
für den Default-Wert verwendet, damit sicher gestellt ist, dass bereits geänderte und möglicherweise aus einem Projektfile geladene Werte auch korrekt angezeigt werden.
Alles, was vom <helppanel />
-Tag eingeschlossen ist, landet wiederum in einem eigenen Tab-Pane , in dem Sinn und Zweck der verschiedenen Ein- und Ausgänge noch einmal kurz erklärt sind. Dieses Panel sollte in keinem Plug-In fehlen, da es als Gedächtnisstütze hilfreich ist und oftmals den Griff zum Manual überflüssig macht.
Ein besonderes Augenmerk soll hier noch auf das Tag <name>numrange</name>
für das Integer-Eingabefeld gelegt werden. Der hier vergebene Name muss innerhalb eines Plug-Ins eindeutig sein, da er benötigt wird, um zu ermitteln, welchen Wert der Benutzer hier eingegeben hat:
OAPC_EXT_API void oapc_set_config_data(void* instanceData,const char *name,const char *value) { struct instData *data; data=(struct instData*)instanceData; if (strcmp(name,"numrange")==0) data->config.numRange=atoi(value); }
So bald das Konfigurationspanel eines Plug-Ins mit name
der eindeutige Name des Eingabefeldes übergeben und in value
der Wert, der vom Benutzer gewählt wurde. Dieser liegt dabei in jedem Fall als Character-Array vor und muss gegebenenfalls entsprechend konvertiert werden. Die Umwandlung in eine Ganzzahl geschieht hier über die Funktion atoi()
.
Noch ein Wort zur oben aufgeführten XML-Struktur: hier wurden zwei Paneltypen und ein Typ für Dateneingaben vorgestellt. Tatsächlich existieren wesentlich mehr Möglichkeiten. Neben zusätzlichen Panels, welche jeweils eigene Namen haben können gibt es auch Auswahlfelder, statische Texte, Eingabefelder für Texte und Fließpunkt-Zahlen, Dateiauswahldialoge, Buttons zur Auswahl einer Farbe und anderes mehr. Damit ist im Prinzip jede Art von Konfigurationsdialog machbar.
Was jetzt noch fehlt, sind Möglichkeiten, die lokal gehaltenen Konfigurationsdaten in ein gemeinsames Projektfile zu bringen bzw. die dort gespeicherten Daten zurück zu erhalten, wenn so eine .apcp-Projektdatei geladen wird. Auch das ist nicht wirklich kompliziert, allerdings gibt es ein paar Kniffe zu beachten.
static struct libio_config save_config; OAPC_EXT_API char *oapc_get_save_data(void* instanceData,unsigned long *length) { struct instData *data; data=(struct instData*)instanceData; *length=sizeof(struct libio_config); save_config.version =htons(1); save_config.length =htons(*length); save_config.numRange=htonl(data->config.numRange); return (char*)&save_config; }
Diese Funktion erwartet als Rückgabewert einen Pointer auf den Speicherbereich, in welchem die zu speichernden Daten liegen und in der Variablen, auf die length
zeigt, deren Größe. Im Prinzip ist hier der Inhalt der zuvor angelegten Struktur libio_config
zurückzugeben – in dieser befindet sich der zu speichernde Parameter numRange
. Da das Plug-In zum einen kompatibel zu möglichen zukünftigen Änderungen bleiben soll und zum anderen auch auf anderen Rechnerarchitekturen funktionieren muss, sind allerdings einige Maßnahmen erforderlich.
So werden zusätzlich eine Versionsnummer für die Datenstruktur und deren Länge gespeichert und in den zu übergebenden Daten abgelegt.
Des weiteren werden alle Member der Struktur libio_config
mittels htons()
und htonl()
in das plattformunabhängige Netzwerk-Datenformat umgewandelt. Dieses dient eigentlich dazu, die direkte Datenübertragung zwischen Rechnern unterschiedlicher Architektur zu ermöglichen, kann aber auch dann problemlos verwendet werden, wenn diese Daten »nur« gespeichert werden. Es ist lediglich zu beachten, das beim Laden die Rückkonvertierung mittels ntohs()
und ntohl()
stattfindet:
OAPC_EXT_API void oapc_set_loaded_data(void* instanceData,unsigned long length,char *loadedData) { struct instData *data; data=(struct instData*)instanceData; if (length>sizeof(struct libio_config)) length=sizeof(struct libio_config); memcpy(&save_config,loadedData,length); data->config.version =ntohs(save_config.version); data->config.length =ntohs(save_config.length); if ((data->config.version!=1) || (data->config.length!=sizeof(struct libio_config))) { // do conversion from earlier versions here } data->config.numRange=ntohl(save_config.numRange); }
Diese Funktion handhabt die umgekehrte Richtung: in loadedData
werden die geladenen Daten mit der Länge length
übergeben und anschließend konvertiert. An dieser Stelle ist bei späteren Versionen des Plug-Ins dann auch zu überpüfen, ob die geladenen Daten eventuell konvertiert müssen.
Damit ist ein wesentlicher Teil des Plug-Ins fertiggestellt. Nachdem es erfolgreich mittels eines simplen Aufrufes von make
in der Konsole compiliert wurde, kann es im Flow-Editor des OpenEditors erstmalig getestet werden.