IP-Symcon Module schreiben

Meine Erfahrungen bei der Erstellung eigener Module an einem Beispiel

Gezeigt wird das grundsätzliche Vorgehen zur Erstellung eines einfachen Device-Modules für IP-Symcon, die Implementierung der Basis und der der spezifischen Funktionalität, die Arbeit mit Properties und Statusvariablen sowie die Kommunikation über Interface

Das DemoModul implementiert ein Textinterface, bei dem Daten von PHP oder von einem passenden Modul (hier Serversocket) geloggt werden kann. So ist z.B. die Übertragung von Daten aus Unix-Systemen über den TCP-Port möglich

Download, für den Sourcecode bitte Disclaimer beachten

Der Sourcecode ist nach meinem besten Wissen fast Zeile für Zeile dokumentiert. Fragen sollten im IP-SymconForum besprochen. Die Screenshots beschreiben jedoch eine ältere Version. werden.

Einrichtung der Arbeitsumgebung

Zur Erstellung von IPSymcon-Modulen wird Delphi2006/2007 oder TurboDelphi benötigt. Alle anderen Versionen gehen leider nicht.

Zunächst ist das SDK (zur Zeit Version V2.3 oder 2.5beta) aus dem IP-Symcon Entwicklerbereich zu laden.


Der Screenshot ist für Version 2.04, es ist jeweils die aktuelle Version auszuwählen.

Das SDK ist dann auszupacken und die Verzeichnisstruktur anzulegen. Unterhalb des ~modules Ordner kann dann das DemoModul ausgepackt werden




Soll das Demo-Modul geklont werden, müssen die Files unter einen neuen Namen kopiert und ein paar manuelle Änderungen im Projektfile gemacht werden. Grundsätzlich ist es eine gute Prxis, für alle Units eindeutige Namen zu vergeben. Es sollten keinesfalls für alle verschiedenen Module immer wieder die gleichen Filenamen gewählt werden. Im bdsproj-File muss eine neue GUID vergeben, mindestens die existierende gelöscht werden. Außerdem ist der neue Name des Projektfiles zu hinterlegen. Alle weiteren Änderungen können dann in der Delphi-Oberfläche gemacht werden.




In Delphi sind nun die Projekteinstellungen zu überprüfen bzw. zu ändern. Als erstes sind die Pfade an die eigene Umgebung anzupassen




Nächster Punkt sind die Einträge in der Versionsverwaltung. Ich verwende diese Einstellung, bei der die Buildversion hoch zählt. Man kann das aber auch komplett deaktivieren.




Zum Debuggen muss man auch die Hostanwendung definieren. Die Pfade müssen angepasst werden. Zum Debuggen darf auf dem PC kein weiteres IPS laufen!




Wenn alles funktioniert, kann man jetzt Module erstellen. An Hand der Statistik sieht man, das trotz relativ wenig eigenem Quelltext eine beachtliche Zahl an zeilen verarbeitet werden muss.




Module für IPSymcon

Module für IPSymcon sind immer DLLs. Zu 90% kann die Basis-Funktionalität eines IPS-Moduls aus einer Vorlage (wie dem hier vorgestellten DemoModul) per Copy/Paste/Replace übernommen werden.

Ich teile den Sourcecode immer in mindestens 3 Files auf.

  1. Projektfile(Endung .dpr)
    Grundlegende Moduldefinition ohne jegliche Funktionalität.
    Beispiel demologger.dpr

  2. Interface-Unit (Uxyz_interface.pas).
    Dort sind die öffentlichen Interfacedefinitionen enthalten. (wie in IPSModulTypes.pas).
    Beispiel UModuleinterface.pas

  3. pro Modul(Splitter bzw. Device) eine eigene Unit (Uxyz_device.pas bzw Uxyz_splitter.pas).
    Diese etwas umfangreichere Unit beeinhaltet bis zum Abschnitt "//---Implementation IIPSDevice Modulfunctionen (Actions)" mit Ausnahme der Get/Set-Funktionen nur Basis-Funktionen, um ein IPS-Modul zu definieren.
    Beispiel UDevice.pas

  4. optional Units mit Hilfsfunktionen bzw aus anderen Programmen übernommenen Logik (gemeinsame Codebasis)

Wichtig ist, das für jedes Modul/Library/Interface immer eigene GUIDs generiert werden (STRG-SHIFT-G), damit es keine Überschneidungen bei mehreren HomeMade Modulen gibt. Aus dem gleichen Grund sollten alle Namen, TypeDefinitionen und Bezeichnungen wie Hersteller, ModulName usw. mit eigenen Namen versehen werden. Sonst haben wir in einem Jahr 10 verschiedene „Logger“-Module :-)))

Funktionen mit komplizierter Logik sollten ersten in StandAlone-Programmen verprobt werden. Lagert man diese Logik in eigene Units aus, aknn man diese meist 1:1 im Modul einsetzen, indem man einfach nur die Funktionen durch Aufrufe der vorhandenen Logik implementiert.

Es können im Prinzip alle in IPS vorhandenen und bekannten Funktionen genauso wie im PHP oder der Konsole auch in Delphi verwendet werden (und umgekehrt) Eine entscheidende Hilfe ist die Befehlsreferenz und die Definitionen in IPSTypes und IPSModulTypes. Geht man in der Delphi-Oberfläche mit dem Cursor auf eine IPS-Funktion, kann man nach Drücken der STRG-Taste auf den entstandenen Link klicken und gelangt so zur Implementierung der Funktion. Das ist oft eine wertvolle Hilfestellung für das Verständnis, was warum passiert.

notwendige Anpassungen für eigene Module

  1. Projektfile
    Kann komplett kopiert werden, nur die eigenen Units,Namen und GUID sind auszutauschen.

    const LibInfo: TIPSLibraryInfo = (
                                     mUniqueID    : '{30dec678-a21a-41f8-840b-cfd52a4a9436}'; //Für jedes Modul mit STRG-SHIFT-G neu erstellen !!
                                     //-------------------------- //nächste 3 Zeilen anpassen, Build muss leider Manuell hizugefügt werden
                                     mAuthor      : 'Dein Name';
                                     mURL         : 'Deine.URL';
                                     mName        : 'DemoLogger Library';
                                     mVersion     : {CompileVersion}$0232{/CompileVersion}; { Hi - MajorV, Lo - MinorV $232=2.50}
                                     mBuild       : {CompileBuild}3{/CompileBuild};
                                     mDate        : {CompileTime}0{/CompileTime};

    ...

    //Register Classes
     ModuleRegistry.RegisterModule(TIPSDevice, TypeInfo(IIPSDevice), 'MyDevice');
  2. Interface-Unit
    Auch diese kleine Datei kann als Vorlage kopiert werden. Für ein neues Modul sind dort ebenfalls die GUIDs und der Interfacename auszutauschen. Zusätzlich müssen die Deklarationen der Interfacefunktionen entsprechend der eigenen Funktionen ausgetauscht werden

    type
    IIPSDevice = interface(IInvokable)
     ['{2097D725-4B79-49CF-887A-1E84C38BEB92}'] //Für jedes Interface mit STRG-SHIFT-G neu erstellen !!
    //Diese Funktionen werden jetzt unter PHP sichtbar
     //setzt Filenamen für Logfile (relativ zu ips.exe)
     procedure setLogFile(FileName:string); stdcall;
     //Gibt einen String an das Modul und gibt die Anzahl der Zeichen zurück
      function setLine(Text:string):integer; stdcall;
      //gibt den letzten Eintrag zurück
      function getLine:string; stdcall;
     end;
  3. Device-Unit
    Die DeviceUnit besteht bis zum Abschnitt "//---Implementation IIPSDevice Modulfunctionen (Actions)" aus Definitionen, die für jedes Modul gelten. So ist es ein guter Startpunkt, zunächst auch diese Datei komplett zu kopieren. Allerdings muss hier etwas mehr Aufwand zur Anpassung an eigene Module getrieben werden. So sollte jedes Modul seinen eigenen Unit-Namen bekommen, das IPS-Objekt bekommt einen eigenen Namen und anschliessend müssen deshalb alle Funktionsköpfe den geänderten Namen bekommen. Letzteres geht am Besten durch ein globales Suchen/Ersetzen auf TIPSDevice; gemacht werden

    Hier ist das IPS-Object definiert. Der Name TIPSDevice muss geändert ...

    //Hauptobjekt des Moduls, implementiert die folgenden Interfaces. 
    TIPSDevice = class(TIPSModuleObject, //standard
                            IIPSModule, //standard
                            IIPSReceiveString, //TextEmpfang aus anderen Modulen ermöglichen
                            IIPSDevice) //eigenes Interface

    ... und dann global ersetzt werden

    constructor TIPSDevice.Create(IKernel: IIPSKernel; InstanceID: TInstanceID);
    
    begin

    Die Felder in TSettings sollten den Properties entsprechen

    //alle persistenten Status-Informationen werden mit get/set hier zugewiesen
      TSettings = record
            Logfile:String;
            echo:boolean;
      end;

    Properties und Status-Variablen werden üblicherweise schon im Constructor definiert

    <
    RegisterStrProperty( 'LogFile',GetLogFileName,SetLogFileName);
     RegisterBoolProperty( 'Echo',GetEcho,SetEcho);
     //oder auch
     //RegisterIntProperty( 'IntVal',GetIntVal,SetIntVal);
    
     {Status-Variablen definieren (optional)
     Diese StatusVariablen werden dann in der Konsole und Frontend automatisch angezeigt
     Die StatusVariablen sollten auch ein profil bekommen, wenn sie in der Anzeige
     formatiert dargestellt werden sollen. Details bitte dem SDK (UIPSTypes ab
     Zeile 1925 für SDK 2.04) entnehmen. Im WebFrontend sind in 2.0.4 aber nur die
     DefaultProfile implementiert, so das man bei eigenen Profile diese dort
     selber nachbilden muss.
     }
     {
     In 2.10 gibt man den Namen eines existierenden Defaultprofiles oder eines eigenen Profiles mit,
     welches man vorher mit CreateXXXXProfile angelegt hat. Bei eigenen Profilen kann man auch ein eigenes Icon
     angeben oder man wählt ein existierendes aus, wie hier.
     }
     TIPSVarProfile.CreateStringProfile('DemoTextProfile', 'Information','', '-->logged');
     RegisterVariable('LastLineVariable','LastText',vtString,'DemoTextProfile');
     //Beispiel für einen Schalter mit default Profile
     //RegisterVariable('StatusVariable','Status',vtBoolean,GetDefaultProfile(dpSwitch));

    Die TextDefinitionen wie Vendor(Herstellerauswahl), Aliases(unter all diesen Namen erscheint das Modul in der Instance-Auswahl) und Modulname müssen angepasst werden

    //------------------------------------------------------------------------------
    class function TIPSDevice.GetVendor(): String;
    //Das Modul erscheint unter diesem Herstellernamen in der Auswahl
    begin
     Result := 'MySelf';
    end;
    
    //------------------------------------------------------------------------------
    class function TIPSDevice.GetAliases(): TStringArray;
    //Das Modul erscheint unter allen diesen Namen in der Auswahl
    begin
    
     SetLength(Result, 2); //Anzahl der Namen entsprechend anpassen
     Result[0] := 'MyDevice';
     Result[1] := 'MyLogger'; //optional
    
    end;
    //------------------------------------------------------------------------------
    class function TIPSDevice.GetModuleName(): String;
    {
    gibt den Namen des Moduls für die Modulverwaltung zurück
    unter diesem Prefix z.B.die Einträge im Log vorgenommen
    }
    begin
     Result := 'MyDemo';
    end;

    letztendlich müssen Typ und Interface und ParentRequirements(if any) definiert werden.

    //------------------------------------------------------------------------------
    class function TIPSDevice.GetModuleType(): TIPSModuleType;
    
    //definiert den Modultyp.
    begin
     Result := mtDevice; //Typ ist i.d.R. mtDevice oder mtSplitter (siehe UIPSTypes)
    end;
    //------------------------------------------------------------------------------
    class function TIPSDevice.GetModuleID(): TStrGUID;
    //gibt die GUID des Moduls für die Modulverwaltung zurück
    begin
     Result := GUIDToString(IIPSDevice); //Will return Interface GUID
    end;

    ...

    //------------------------------------------------------------------------------
    class function TIPSDevice.GetParentRequirements(): TStrGUIDs;
    
    //Abfrage, ob der Parent ein kompatibles Interface hat
    begin
     SetLength(Result, 1);
     Result[0] := GUIDToString(IIPSSendString);
    
    end;
    //------------------------------------------------------------------------------
    class function TIPSDevice.GetImplemented(): TStrGUIDs;
    
    //Interface-Kompatibilität
    begin
      SetLength(Result, 1);
     Result[0] := GUIDToString(IIPSReceiveString);
    end;

    ...

    //Check Parent
     RequireParent(IIPSSendString,false);

    Letztendlich fehlen noch die GetSet-Funktionen für die Properties

    // Implementation Get/Set-Funktionen
    //------------------------------------------------------------------------------
    //Get Funktion für StringProperty Logfile
    function TIPSDevice.getLogFileName:string;stdcall;
    begin
        //Nur zur Demo!! Eintrag im Kernellog zum Nachvollzienen der Aufrufe
     LogMessage(KL_DEBUG,'Read Property Logfile...');
    
        result:=fchangedsettings.Logfile;
    end;
    //------------------------------------------------------------------------------
    //Set Funktion für StringProperty Logfile
    procedure TIPSDevice.setLogFileName(FileName:string);stdcall;
    begin
        //Nur zur Demo!! Eintrag im Kernellog zum Nachvollzienen der Aufrufe
     LogMessage(KL_DEBUG,'Set Property Logfile');
    
       if (FileName=fchangedSettings.Logfile) then
       begin
         exit; //nichts zu tun, wenn wir das Gleiche schon haben
       end;
        fchangedsettings.Logfile:=FileName; //ablegen
    
        {setzt ein Flag zum Speichern und
        erzeugt eine SettingsChanged-Message}
        settingschanged;
    end;
    /Get Funktion für BoolProperty echo
    function TIPSDevice.getEcho;stdcall;
    begin
        //Nur zur Demo!! Eintrag im Kernellog zum Nachvollzienen der Aufrufe
     LogMessage(KL_DEBUG,'Read Property Echo...');
        result:=fchangedsettings.echo;
    end;
    //------------------------------------------------------------------------------
    //Set Funktion für BoolProperty Echo
    procedure TIPSDevice.setEcho(bEcho:boolean);stdcall;
    begin
        //Nur zur Demo!! Eintrag im Kernellog zum Nachvollzienen der Aufrufe
     LogMessage(KL_DEBUG,'Set Property Echo');
    
       if (bEcho=fchangedSettings.echo) then
       begin
         exit; //nichts zu tun, wenn wir das Gleiche schon haben
       end;
        fchangedsettings.echo:=bEcho; //ablegen
    
        {setzt ein Flag zum Speichern und
        erzeugt eine SettingsChanged-Message}
        settingschanged;
    end;
  4. Die eigentliche Funktionalität muss hereingebracht werden --)))
    Diese habe ich der Funktionsbeschreibung kommentiert

Beschreibung des Demo-Moduls

Komponenten:

Kompatibilität und Voraussetzungen

Modul Funktionalität



Modul Umsetzung




Damit ist die komplette Modulfunktionalität implementiert!!

Der restliche Quellcode ist im wesentlichen nur noch formelle Modulimplementation



Modul Installation

Die Datei demologger.dll muss in das IP-Symcon Modules Verzeichnis kopiert werden. Anschließend muss der Dienst neu gestartet werden.

Modul Konfiguration und Inbetriebnahme:

Es muss zuerst eine Instanz des Modul angelegt werden. Dazu (entsprechend der jeweils gültigen IP-Symcon-Dokumentation ) in der Verwaltungskonsole "Objekt hinzufügen"->"Instanz-Hinzufügen" auswählen. Dort erst den Hersteller, dann kann das entsprechende Modul selektiert und der Dialog mit OK verlassen werden.




Da bisher kein passender Parent (Modul-Instance mit SendString-Interface) existiert, sollte jetzt einer angelegt werden.




Zunächst wird eine Auswahl kompatibler Instancen angezeigt. Diese Funktionalität wird durch die Informationen aus RequireParent sowie GetParentRequirements und GetImplemented bereitgestellt. Wir wollen zur Demo eine Instance des ServerSocket-Modul erstellen. Das Modul wäre aber ohne irgendeine Änderung auch an den anderen Modulen betreibbar.




Diese Instance wird auch noch mit einem passenden TCP-Port konfiguriert. Dazu muss „Öffne Server“ aktiviert sein. Anschließend auf „übernehmen“ und „OK“ klicken


Zurück in der Modulkonfiguration muss man erst auf „Übernehmen“ und dann auf „OK“ klicken.







Jetzt sollte das Modul incl. Statusvariable in der Konsole sichtbar sein


Anschließend ist mittels eines kleinen PHP-Scriptes der Filename für das Logging einzugeben. Das ist ein guter Test um zu sehen, ob die definierten PHP-Funktionen sauber implementiert worden sind. Dazu ist ein neues Script anzulegen. Anschließend im Editor auf „Befehl hinzufügen“ klicken und das neu erstellte Device auswählen. Nach dem Klick auf „Weiter“ sollten die definierten Funktionen auftauchen.

Dieser Assistent bietet schon (auch ohne eigene Programmierung!) einen passenden Eingabescreen an.




Weil es so schön ist, fügen wir gleich noch einen zweiten Befehl hinzu




Im Editor kann man auch nach Tippen der Anfangsbuchstaben, gefolgt von STRG-Leertaste, ein Fenster mit den Funktionen bekommen.






Bei einer richtigen Konfiguration ist von nun an ein Datenempfang und Verarbeitung möglich.Wenn alles geklappt hat, liefert das Script auch die erwartete Ausgabe. Auch die Ersetzung durch das Modul (Funktion HandleData) wurde korrekt vorgenommen




Auch die Status-Variable hat nun den richtige Wert:




Als Krönung wollen wir jetzt noch sehen, ob die Daten vom ServerModul genauso angenommen werden. Man kann mit Telnet eine Verbindung herstellen, aber das macht wenig Sinn(jedes Zeichen eine Zeile). Ich habe es trotzdem mal gemacht->Siehe Textlog am Ende). Interessanter ist jedoch die Kommunikation mit anderen Rechnern. Dazu wird auf einem Linux-Rechner eine Abfrage auf den USV-Status gemacht und die Ausgabe über netcat(nc) eine TCP-Verbindung zum eigenen ServerSocket-Modul in IPSymcon hergestellt. Es sind aber auch beliebig andere Funktionen möglich.





Zum Test benutzen wir erst ein Script, dann wird im Logfile nachgesehen. Alles klappt wie gewünscht.




Referenz



1. MyModule

Funktion

DemoModul mit TextLogger-Funktion
Text über das Interface oder von PHP wird im Logfile und in der Statusvariable gespeichert.

Status-Variablen

Konfiguration



PHP-Funktionen

Alle Funktionen haben im PHP den Präfix "MyDevice_"

Beispiel: MyDevice_SetLogFile(InstanceID,"mylog.txt"); //Log to %IP-Symcon%\mylog.txt
Beispiel: echo MyDevice_SetLine(instanceID,"Hallo PHP"); //Ausgabe:8
Beispiel: echo MyDevice_getLine(instanceID); //Ausgabe:PHP -->Hallo PHP

Stand Dokumentation: Version 2.10 10.09.2009

Index
Disclaimer

© 2009-2011 Thomas Dreßler
Alle Rechte vorbehalten
letzte Änderung 07.12.2011