Script Interface Plugin Development Guide

Beispielcode Script

// Finde den Node über den Node-Pfad.
const pressureNode = codabix.findNode("/Nodes/Injection molding/Pressure", true);
 
// Erstelle einen Intervalltimer, der unsere Funktion alle 3 Sekunden aufruft.
timer.setInterval(function () {
 
    // Generiere eine zufällige Zahl zwischen 20 und 150.
    let randomNumber = Math.random() * 130 + 20;
 
    // Schreibe diese Zahl nun in die Node.
    codabix.writeNodeValueAsync(pressureNode, randomNumber);
 
}, 3000);
Die CoDaBix Webkonfiguration enthält den Menüpunkt „Script Interface“, mit dem Sie Scripts erstellen, editieren und löschen können. Sie können auch Scripts stoppen, sodass sie nicht weiter ausgeführt werden, ohne sie zu löschen.

Script Interface

Eigenschaft Beschreibung
Name Name des Scripts, um es zu identifizieren. Dieser Name wird auch in Stapelüberwachungen benutzt, wenn eine Ausnahme aufgetreten ist.
Description Hier können Sie eine detailliertere Beschreibung des Scripts eingeben.
Editor Strictness Level Bestimmt, wie streng der Editor bestimmten Code handhabt.

Low (Standard): Der Editor bemängelt die bei den folgenden Optionen beschriebenen Fälle nicht.
Medium: Der Editor bemängelt implizite any-Typen, implizite Returns sowie Fallthrough-Cases in switch-Anweisungen.
High: Zusätzlich zu den Fällen des „Medium“-Levels bemängelt der Editor nicht verwendete lokale Variablen sowie nicht verwendete Funktionsparameter.

Beachten Sie: Für Library Scripts (in einer späteren Version verfügbar) ist das Level immer „High“.
Script State Enabled: Das Script soll ausgeführt werden.
Disabled: Das Script soll gestoppt werden.
CurrentScriptState Zeigt den aktuellen Zustand des Scripts an.

NotRunning: Das Script wird nicht ausgeführt, entweder weil es auf „Disabled“ gesetzt ist, oder weil es kürzlich erst erstellt wurde und noch kein Live-Code dafür existiert.
Running: Das Script wurde gestartet und seitdem ist kein Fehler aufgetreten (das kann auch der Fall sein, wenn das Script vor kurzem wegen einer unbehandelten Ausnahme neugestartet wurde).
Stopped: Das Script wurde gestartet und ausgeführt, aber es sind keine Event-Listener oder Callbacks mehr aktiv.
StoppedAndScheduledForRestart: Das Script wurde wegen einer unbehandelten Ausnahme während der Ausführung gestoppt und wird nach einer kurzen Zeit automatisch neugestartet.

Der Standartwert für „Editor Strictness Level“ ist „Low“. Wir empfehlen diese Einstellung, wenn Sie gerade erst mit Scripts und JavaScript anfangen. Wenn Sie ein erfahrener TypeScript Entwickler sind, empfehlen wir die Einstellung „Medium“ oder „High“, sodass der Editor Sie dabei unterstützen kann, eine saubere Codegrundlage zu entwickeln.

Beachten Sie: Es kann bis zu 3 Sekunden dauern, bis eine Änderung (z.B. Starten oder Stoppen eines Scripts) in Kraft tritt und weitere 3 Sekunden, bis der CurrentScriptState und das Script Log aktualisiert werden.

Nachdem Sie eine Liveversion (siehe Abschnitt Going Live) eines Scripts erstellt haben, wird diese automatisch gestartet, solange dessen Status auf „Enabled“ gestellt ist. Beim Starten befindet sich das Script in der sogenannten „Initialisierungsphase“. Während dieser Phase kann das Script Callbacks registrieren (z.B. um Ereignisse oder Timer zu behandeln). Wenn einer der Callbacks aufgerufen wird, befindet sich das Script in der sogenannten „Callback-Phase“ (auch in dieser Phase kann es noch weitere Callbacks registrieren).

Das folgende Diagramm veranschaulicht die Phasen eines Scripts:

Script Phasen

Beachten Sie: Obwohl es im obigen Diagramm nicht dargestellt wird, kann das Script immer noch weitere Callbacks für andere Events registrieren, wenn es sich bereits in der Callback-Phase befindet.

Auf das Script wird ein Timeout von ungefähr 15000 ms angewandt, damit unbeabsichtigte Endlosschleifen wie while (true) {} das System nicht lahmlegen können. Wenn das Script nach einer Zeitüberschreitung noch nicht fertig ist, wird es gestoppt und so behandelt, also ob eine unbehandelte Ausnahme aufgetreten ist.

Sowohl in der Initialisierungs- als auch in der Callback-Phase wird das Script beim Auftreten einer unbehandelten Ausnahme nach einer kurzen Zeit (ca. 3 Sekunden) automatisch neu gestartet.

Durch Klick auf das Scripteditor-Icon (Script Editor Icon) öffnet sich der Editor und Sie können Ihren Scriptcode schreiben.

Script Code bearbeiten

Wenn Sie bereits mit Visual Studio oder VS Code gearbeitet haben, wird Ihnen der Script-Editor bekannt vorkommen (tatsächlich basiert dieser auf dem Monaco-Editor von VS Code). Der Editor stellt während des Tippens IntelliSense für CoDaBix API-Methoden bzw. Interfaces sowie für eingebaute JavaScript-Klassen zur Verfügung, wie Sie im obigen Screenshot sehen können.

Wenn Sie mit der Maus über Variablen- oder Methodenaufrufe fahren, erscheint ein Tooltip, der den Typ anzeigt:

Typen

Wenn das Script einen Fehler enthält, unterringelt der Editor diesen Fehler rot und zeigt den Fehler beim Darüberfahren mit der Maus an:

Fehler

Wenn Sie an einer Stelle im Code rechtsklicken, erscheint ein Kontextmenü mit nützlichen Befehlen. Beispielsweise können Sie alle Referenzen zu einer bestimmten Variable im Code ermitteln (und diese z.B. auch umbenennen):

Kontextmenü

Going Live

Im Script-Editor können Sie den Code für ein Script schreiben und speichern („Entwurf“), ohne dass dieser Entwurf ausgeführt wird. Erst wenn Sie „Go Live“ auswählen, wird der aktuelle Entwurf als „Live-Version“ gespeichert und tatsächlich ausgeführt. Dies ermöglicht Ihnen, schrittweise an einem Scriptcode zu arbeiten, ohne die momentan ausgeführte Live-Version zu beeinflussen. Mit dem Button „Toggle Diff Editor“ können Sie zu einem Diff-Editor wechseln, der einen Vergleich der Änderungen zwischen der Live-Version und dem aktuellen Entwurf anzeigt.

Script Bearbeitungsoptionen

Sobald Sie mit der Editierung des Entwurfs fertig sind, wählen Sie die Checkbox „Go Live“ aus und klicken auf den Speichern-Button. Dadurch wird der aktuelle Entwurf zur Live-Version, sodass dieser tatsächlich ausgeführt wird.

Wenn ihr Script zur Entwurfszeit einen Fehler enthält, während Sie versuchen, dies als Live-Version zu speichern, erscheint eine Dialogbox mit dem Fehler:

Fehlerfeld

Andernfalls wird der Scripteditor geschlossen und der neue Scriptcode nach ein paar Sekunden ausgeführt.

Nützliche Tastenkombination

  • Strg+F: Suchen/Ersetzen
  • Strg+F2: Umbenennen (scriptweit)
  • Umschalt+F12: Alle Verweise suchen
  • Strg+F12: Gehe zu Definition
  • Strg+K, Strg+C: Auswahl auskommentieren
  • Strg+K, Strg+U: Auskommentierung der Auswahl aufheben
Jedem Script ist eine Logdatei zugeordnet. Sobald ein Script gestartet wurde (oder eine unbehandelte Ausnahme aufgetreten ist), wird ein Eintrag in die Logdatei gemacht. Zusätzlich können Sie einen Logeintrag direkt aus dem Scriptcode heraus über den Aufruf logger.log() erstellen.
Wenn Sie auf den Script-Log-Button (Log-Button) klicken, erscheint ein Dialogfenster mit dem Inhalt der Logdatei. Falls beispielsweise das Script gestartet wurde, aber eine unbehandelte Ausnahme in einem Callback auftrat, könnte das Log wie folgt aussehen:

Beispiellog

Wenn eine Ausnahme auftritt, enthält der Logeintrag eine Stapelnachverfolgung (Stacktrace), in dem die Zeilennummern des Scripts angezeigt werden (nach dem Doppelpunkt), die angeben, an welcher Stelle im Code die entsprechenden Funktionen zum Zeitpunkt des Auftretens der Ausnahme ausgeführt wurden.

Beachten Sie: Wenn eine unbehandelte Ausnahme auftritt, wird diese auch im Runtime Log angezeigt:

Fehlermeldung

JavaScript-Grundlagen

Im Folgenden finden Sie eine kurze Zusammenfassung der JavaScript-Grundlagen. Für ein detaillierteres Tutorial besuchen Sie bitte den JavaScript Guide auf MDN.

In einem Script können Sie Variablen, die Werte aufnehmen können, mit let und const deklarieren (const bedeutet, dass die Variable nicht verändert werden kann). Sie können Werte über den „=“-Operator zuweisen (wohingegen „==“ zum Prüfen auf Gleichheit verwendet wird):

let anzahl = 123;
const meinText = "Hallo Welt!";

JavaScript unterstützt eine Reihe an grundlegenden Werttypen:

  • number: Ein Number (Gleitkommazahl mit doppelter Genauigkeit) kann sowohl Ganzzahlen als auch Dezimalzahlen speichern. Sie können Numbers für Berechnungen verwenden, z.B.:

    let ergebnis = (2 + 0.5) * 3;   // 7.5
  • boolean: Ein Boolean ist entweder true (wahr) oder false (falsch). Ein Boolean entsteht beispielsweise als Ergebnis eines Vergleichs und kann für Kontrollfluss-Anweisungen wie if, while usw. verwendet werden.
  • string: Ein String kann aus einer beliebigen Anzahl an Zeichen (Characters) bestehen und wird verwendet, um Text zu speichern. Strings können mit dem „+“-Operator zusammengefügt werden:

    let anzeigeText = "Das Ergebnis von 5+6 ist " + (5+6);   // "Das Ergebnis von 5+6 ist 11"
  • object: Ein Objekt speichert Eigenschaften („Properties“), die aus einem Schlüssel (String) und einem Wert (beliebiger Typ) bestehen. So enthält beispielsweise das codabix-Objekt Eigenschaften, die auch Methoden sind, wie findNode. Auf Objekt-Eigenschaften wird meist über die Punkt-Notation (.) zugegriffen (codabix.findNode(…), codabix.rootNode, …).


Sie können Kontrollfluss-Anweisungen verwenden, um Vergleiche durchzuführen:

let ergebnis = "Der Wert ist "
if (anzahl > 200) {
    ergebnis += "größer als";
}
else if (anzahl < 200) {
    ergebnis += "kleiner als";
}
else {
    ergebnis += "gleich";
}
ergebnis += " 200.";


Sie können Funktionen erstellen, die wiederverwendbaren Code enthalten. Beispielsweise könnten Sie eine Funktion erstellen, die den Durchschnitt aus zwei numerischen Werten berechnet:

function durchschnitt(wert1, wert2) {
    return (wert1 + wert2) / 2;
}
 
// Berechne den Durchschnitt aus 100 und 250 und schreibe ihn ins Script-Log
logger.log("Der Durchschnitt aus 100 und 250 ist " + durchschnitt(100, 250) + ".");

Wenn Sie diesen Code ausführen, schreibt dieser in das Script-Log soetwas wie:

2016-09-28 14:57:41.7 Z: [Log] Der Durchschnitt aus 100 und 250 ist 175.

Script API

Das Script-Interface stellt die folgenden API-Namespaces zur Verfügung, die in einem Script genutzt werden können:

  • codabix: Enthält die CoDaBix-spezifische Funktionalität, z.B. um auf Nodes zuzugreifen und diese zu verändern.
  • timer: Enthält Methoden um einen Timer zu erstellen, welcher eine von Ihnen im Script definierte Funktion (regelmäßig) nach einer angegebenen Zeit aufruft.
  • logger: Enthält eine log-Methode, mit der Sie in das Script-Log schreiben können.
  • storage: mit dem Storage-Objekt können Sie Informationen speichern, die auch nach einem Neustart des Scripts erhalten bleiben.
  • io: Unterstützt I/O-Vorgänge, z.B. Dateizugriff.
  • net: Unterstützt netzwerkspezifische Vorgänge, z.B. das Registrieren von HTTP-Handlern.
  • runtime: Enthält Funktionen zum Interagieren mit der Script-Laufzeitumgebung.

Beachten Sie: Der Script-Editor unterstützt IntelliSense, sodass Sie die im codabix-Namespace vorhandenen Methoden sehen können, indem Sie codabix. schreiben (beachten Sie den Punkt nach „codabix“). Auch wenn eine Methode ein Objekt (wie eine Node) zurückgibt, können Sie wiederrum einen Punkt eintippen, um zu sehen welche Methoden dieses hat.

BEACHTEN SIE: Solange CoDaBix 1.0.0 noch nicht freigegeben wurde, wird die Script-API als nicht stabil betrachtet und kann bis dahin noch geändert werden.

Auf CoDaBix zugreifen

Einen Node finden und dessen Wert protokollieren

Nehmen wir an, dass Sie CoDaBix mit dem „Demo-Data (Continuous)“-Plugin installiert haben und nun auf die Node Nodes → Demo-Data → Temperature zugreifen möchten. Dazu müssen Sie zuerst den Node-Path oder den Identifier der Node abrufen. Öffnen Sie dazu bitte in der CoDaBix Webkonfiguration die Node-Ansicht, wählen Sie die entsprechende Node aus und klicken Sie auf das Access-Symbol (). Anschließend kopieren Sie bitte den „Absolute Node Path“. Wir übergeben diesen Pfad dann an die codabix.findNode()-Methode sowie den Parameter true, damit die Methode eine Ausnahme wirft, falls die Node nicht gefunden werden konnte:

// Finde die "Temperature"-Node und überprüfe, ob die Node
// einen Wert hat.
const temperatureNode = codabix.findNode("/Nodes/Demo-Nodes/Temperature", true);
if (temperatureNode.value != null) {
    // OK, Node hat einen Wert. Nun protokollieren wir diesen Wert.
    logger.log("Aktuelle Temperatur: " + temperatureNode.value.value);
}

Ihr Script-Log könnte dann wie folgt aussehen:

2016-09-28 15:08:45.2 Z: Started.
2016-09-28 15:08:45.3 Z: [Log] Aktuelle Temperatur: 71
2016-09-28 15:08:45.3 Z: Stopped.

Allerdings wird in diesem Beispiel nur ein einziger Wert protokolliert. Der Grund dafür ist, dass nach dem Starten des Scripts der Code ausgeführt wird, welcher die Node sucht und deren Wert protokolliert, doch danach ist das Script zu Ende.

Wenn wir nun den Wert nicht nur einmal, sondern alle 5 Sekunden protokollieren möchten, können wir dies erreichen, indem wir einen Timer erstellen und diesem eine Funktion mitgeben, welche dieser regelmäßig nach einem Intervall aufruft (Beachten Sie: Für Callbacks verwenden Sie am besten statt function () {...} eine Fat-Arrow-Function: () => {...}).

// Finde den "Temperature"-Node.
const temperatureNode = codabix.findNode("/Nodes/Demo-Nodes/Temperature", true);
 
// Nun erstellen wir einen Timer, der den Wert des
// Nodes alle 5 Sekunden protokolliert.
const interval = 5000;
timer.setInterval(() => {
 
    // Wenn der Node einen Wert hat, protokollieren wir diesen.
    if (temperatureNode.value != null) {
        logger.log("Aktuelle Temperatur: " + temperatureNode.value.value);
    }
 
}, interval);

Wenn Sie dieses Script ausführen, könnte Ihr Script-Log wie folgt aussehen:

2016-09-28 15:15:42.6 Z: Started.
2016-09-28 15:15:47.6 Z: [Log] Aktuelle Temperatur: 70
2016-09-28 15:15:52.6 Z: [Log] Aktuelle Temperatur: 75
2016-09-28 15:15:57.6 Z: [Log] Aktuelle Temperatur: 63
2016-09-28 15:16:02.6 Z: [Log] Aktuelle Temperatur: 71

Nodeereignisse

Das obige Beispiel verwendet einen Timer, der eine Script-Funktion regelmäßig aufruft. Es ist jedoch auch möglich, sich für bestimmte Ereignisse (Events) eines Nodes zu registrieren:

  • ValueChanged: Wird ausgelöst, wenn ein Wert in den Node geschrieben wurde (value-Eigenschaft).
    Beachten Sie: Dieses Event wird auch ausgelöst, wenn der neue Wert gleich dem alten Wert ist. Um festzustellen, ob sich der Wert tatsächlich geändert hat, können Sie die isValueChanged-Property des Listener-Arguments prüfen.
  • PropertyChanged: Wird ausgelöst, wenn sich eine Eigenschaft des Nodes (außer der value-Eigenschaft) ändert, wie z.B. name, displayName usw.
  • ChildrenChanged: Wird ausgelöst, wenn unterhalb des aktuellen Nodes eine oder mehrere Kindnodes hinzugefügt oder entfernt werden.

Sie können auf Events reagieren, indem Sie einen Eventlistener (Callback) zu dem Node hinzufügen, dessen Ereignis Sie behandeln möchten.
Beispiel:

// Finde den "Temperature"-Node und füge einen Handler für das "ValueChanged"-Ereignis hinzu.
const temperatureNode = codabix.findNode("/Nodes/Demo-Nodes/Temperature", true);
 
temperatureNode.addValueChangedEventListener(e => {
 
    // Protokolliere den alten und den neuen Wert des Nodes.
    logger.log("Alter Wert: " + (e.oldValue && e.oldValue.value)
        + ", Neuer Wert: " + e.newValue.value);
 
});

Beachten Sie: Innerhalb eines Node-Eventlisteners können Sie keinen Nodewerte (synchron) lesen oder schreiben oder andere Änderungen an Nodes vornehmen. Wenn Sie dies tun möchten, benutzen Sie bitte codabix.scheduleCallback(), um einen Callback anzugeben, der so schnell wie möglich nach dem Verlassen des Eventlisteners ausgeführt werden soll.

Werte in einen Node schreiben

Sie können aus einem Script heraus auch Werte in einen Node schreiben. Für das folgende Beispiel wählen Sie bitte in der Node-Ansicht der CoDaBix Webkonfiguration den Node „Nodes“ aus und erstellen einen Datenpunkt-Node mit dem Namen „Zähler“, und wählen für die Option „History Options“ on Value Change aus. Anschließend erstellen Sie eine Script mit diesem Code:

const zählerNode = codabix.findNode("/Nodes/Zähler", true);
 
// Deklariere eine Zähler-Variable.
let meinZähler = 0;
 
// Erstelle einen Callback, der den Zähler erhöht und den aktuellen
// Wert in einen Node schreibt, solange bis der Zähler 5 ist.
let timerID = timer.setInterval(() => {
    meinZähler = meinZähler + 1;
    codabix.writeNodeValueAsync(zählerNode, meinZähler);
 
    if (meinZähler == 5)
        timer.clearInterval(timerID);
}, 500);

Wechseln Sie nun wieder zur Node-Ansicht, wählen den „Zähler“-Node aus und öffnen Sie die historischen Werte ():

Hier können Sie sehen, dass die Werte 1, 2, 3, 4, 5 in die Node in einem Abstand von 0,5 Sekunden geschrieben wurden.

Beachten Sie: Es gibt auch einen einfacheren Weg, diesen Code zu schreiben (ohne Callbacks), und zwar über eine asynchrone Funktion (async function), wie es im nächsten Kapitel gezeigt wird.

Asynchrone Funktionen

Das Beispiel aus dem vorherigen Kapitel erstellt einen Timer, der einen Callback aufruft. Solcher Code kann jedoch schnell unübersichtlich werden, wenn man komplexere Bedingungen hat. Asynchrone Funktionen (Async Functions) ermöglichen Ihnen, diesen Code zu vereinfachen, da er wie synchroner Code (ohne Callbacks) aussieht. CoDaBix stellt einige asynchrone Funktionen bereit, die Sie daran erkennen können, dass ihr Name auf Async endet.
Beispielsweise kann nun der obige Code durch die Verwendung der Funktion timer.delayAsync so umgeschrieben werden:

const zählerNode = codabix.findNode("/Nodes/Zähler", true);
 
// Schreibe die Werte 1 bis 5 in den Node, und warte dazwischen jeweils 0,5 Sekunden.
for (let i = 1; i <= 5; i++) {
    await codabix.writeNodeValueAsync(zählerNode, i);
    await timer.delayAsync(500);
}

Beachten Sie die Verwendung des Schlüsselwortes await. Await (Abwarten) bedeutet soetwas wie „Unterbrich die Ausführung an der aktuellen Stelle solange, bis die asynchrone Operation der Funktion abgeschlossen ist“. Die delayAsync-Funktion gibt hier ein Promise-Objekt („Versprechen“) zurück, welches nach dem Verstreichen von 0,5 Sekunden aufgelöst wird. Sobald das Promise aufgelöst ist, wird die Ausführung fortgesetzt. Andere asynchrone Funktionen geben ebenfalls Promise-Objekte zurück, welche ggf. mit Werten aufgelöst werden können.

Wenn Sie await weglassen, würde Ihr Code nicht 0,5 Sekunden warten, sondern sofort mit dem nächsten Schleifendurchlauf weitermachen. Bitte beachten Sie aber, dass während des „Abwartens“ einer asynchronen Operation mit await anderer Code ausgeführt werden kann; beispielsweise könnte ein Event einer Node ausgelöst werden, während Ihr Code an der await-Position wartet.

Im obigen Beispiel wird await auch benutzt, um writeNodeValueAsync abzuwarten. Das Schreiben von Nodewerten kann einige Zeit dauern, wenn die Node an ein Gerät (wie eine S7) gekoppelt ist. In diesem Fall würde die Ausführung solange warten, bis der Wert tatsächlich in das Gerät geschrieben wurde. Wenn Sie darauf nicht warten möchten, können Sie das await hier auch weglassen (wobei Sie in dem Fall den void-Operator vor den Funktionsaufruf setzen müssen, um dem Compiler zu signalisieren, dass das zurückgegebene Promise-Objekt absichtlich verworfen wurde).

Wenn Sie nun allerdings diesen Code direkt ausprobieren, wird er noch nicht funktionieren, denn damit man await benutzen kann, muss die umgebende Funktion mit dem async-Schlüsselwort gekennzeichnet sein. Wenn diese umgebende, asynchrone Funktion die „Hauptfunktion“ ist, umschließen Sie diese am besten mit einem Aufruf von runtime.handleAsync(), damit unbehandelte Ausnahmen nicht lautlos „verschluckt“ werden:

Async-Template.js
runtime.handleAsync(async function () {
 
    // Ihr Code...
 
} ());

Hier ist ein komplettes Beispiel eines Scripts, das asynchrone Funktionen benutzt:

runtime.handleAsync(async function () {
 
    for (let i = 0; i < 10; i++) {
        logger.log("Durchlauf " + i);
        await timer.delayAsync(1000);
    }
 
} ());

Beachten Sie: Wenn Sie eine asynchrone Funktion als Callback für einen Eventlistener verwenden möchten, umschließen Sie diese am besten ebenfalls in einen runtime.handleAsync-Aufruf wie im folgenden Beispiel, in welchem das Ereignis behandelt wird, wenn die Node einen neuen Wert bekommt:

const meineNode = codabix.findNode("/Nodes/A", true);
 
myNode.addValueChangedEventListener(() => runtime.handleAsync((async () => {
    // Führe asynchronen Code aus...
}) ()));

Nodewerte mit einem synchronen Lesevorgang lesen

Ein weiteres Beispiel einer asynchronen Funktion ist codabix.readNodeValuesAsync(). Diese Methode führt einen synchronen Lesevorgang auf dem Gerät aus, und das Lesen von Werten vom Gerät kann einige Zeit dauern. Deshalb sollten Sie await verwenden, um die Ausführung an der aktuellen Stelle zu unterbrechen, bis die gelesenen Werte eintreffen:

runtime.handleAsync(async function () {
 
    const node1 = codabix.findNode("/Nodes/A", true);
    const node2 = codabix.findNode("/Nodes/B", true);
 
    // Führe einen synchronen Lesevorgang aus, und warte ab, bis die Werte vom Gerät eintreffen.
    let [nodeValue1, nodeValue2] = await codabix.readNodeValuesAsync(node1, node2);
 
    logger.log("Node1 Wert: " + (nodeValue1 && nodeValue1.value)
        + ", Node2 Wert: " + (nodeValue2 && nodeValue2.value));
 
} ());

Dateizugriff

Die Namespaces io.file, io.directory und io.path enthalten Methoden und Klassen zum Arbeiten mit Dateien und Verzeichnissen. Beispielsweise können Sie Textdateien lesen und schreiben, Dateien kopieren, verschieben oder löschen, sowie alle Dateien in einem Verzeichnis auflisten.
Beachten Sie, dass der Dateizugriff den Einschränkungen der File Access Security unterliegt, die in den CoDaBix Einstellungen definiert wurden.

Die meisten I/O-Operationen sind als asynchrone Funktionen implementiert, die ein Promise-Objekt zurückgeben. Der Grund dafür ist, dass die I/O-Operationen einige Zeit in Anspruch nehmen können, bis sie abgeschlossen sind (dies hängt auch vom Dateisystem ab). Damit CoDaBix währenddessen nicht blockiert ist, werden die I/O-Operationen im Hintergrund ausgeführt. Sie können diese in asynchronen Funktionen über das await-Schlüsselwort aufrufen.

Beachten Sie: Unter Windows 10 Version 1511 und älter (sowie Windows Server 2012 R2 und älter), z.B. unter Windows 7, ist die maximale Pfadlänge auf 260 Zeichen (MAX_PATH) begrenzt.
Unter Windows 10 Version 1607 und höher (sowie Windows Server 2016 und höher) können Sie längere Pfade benutzen. Dazu müssen Sie jedoch erst die Einstellung „Lange Win32-Pfade aktivieren“ in den Windows-Gruppenrichtlinien aktivieren, siehe Enabling Win32 Long Path Support.

Grundlegende Dateioperationen

Aufzählen der Dateien im CoDaBix Data „log“-Verzeichnis:

runtime.handleAsync(async function () {
 
    // Rufe den Pfad zum Codabix "log"-Verzeichnis ab. Dafür benutzen wir die durch CoDaBix
    // definierte Umgebungsvariable "%CodabixDataDir%".
    // Mit combinePath können Pfad-Elemente unabhängig vom Betriebssystem kombiniert werden.
    const codabixLogDir = io.path.combinePath(
            runtime.expandEnvironmentVariables("%CodabixDataDir%"), "log");
    const fileList = await io.directory.getFilesAsync(codabixLogDir);
 
    let result = "";
    for (let file of fileList) {
        result += "File: " + file + "\n";
    }
 
    logger.log("Files in " + codabixLogDir + ":\n" + result);
 
} ());

Das Ergebnis könnte wie folgt aussehen:

  • io.path.combinePath(): Kombiniert zwei oder mehr Zeichenfolgen zu einem Pfad, sodass dies unabhängig vom Betriebssystem funktioniert. Beispielsweise ist das Pfad-Trennzeichen unter Windows \, während es unter Linux / ist. Diese Methode nutzt automatisch das richtige Trennzeichen zum Kombinieren der Zeichenfolgen.
  • io.directory.getFilesAsync(): Gibt ein string[]-Array zurück, welches die Namen der Dateien des angegebenen Verzeichnisses enthält (io.directory.getDirectoriesAsync() gibt entsprechend die Namen der Unterverzeichnisse zurück).
  • io.file.copyFileAsync(): Kopiert eine Datei.
  • io.file.moveFileAsync(): Verschiebt eine Datei oder benennt eine Datei um.
  • io.file.deleteFileAsync(): Löscht eine Datei.
  • runtime.expandEnvironmentVariables(): Ersetzt den Namen jeder Umgebungsvariablen (eingeschlossen in zwei %-Zeichen) im angegebenen String durch deren Wert. CoDaBix definiert die folgenden Umgebungsvariablen zusätzlich zu denen des Betriebssystems:
    • CodabixDataDir: Enthält den Pfad zum aktuell verwendeten CoDaBix-Datenverzeichnis.
    • CodabixInstallDir: Enthält den Installationspfad von CoDaBix.

Lesen und Schreiben von Textdateien

Eine Textdatei in einem Schritt schreiben:

runtime.handleAsync(async function () {
 
    // Erstelle einen String und schreibe ihn in eine Textdatei (HelloWorld.txt).
    let filePath = io.path.combinePath(runtime.expandEnvironmentVariables("%CodabixDataDir%"),
            "Temp", "HelloWorld.txt");
    let content = "Hello World from CoDaBix!\r\n\r\n" +
            "Current Time: " + new Date().toLocaleString();
 
    await io.file.writeAllTextAsync(filePath, content);
 
} ());

Dieses Script erstellt die Textdatei HelloWorld.txt im Temp-Verzeichnis Ihres CoDaBix-Datenverzeichnis (welches automatisch erstellt wird, wenn es noch nicht existiert). Die Datei könnte dann so aussehen:

Durch den Aufruf von io.file.writeAllTextAsync() wird die Datei ein einem Schritt geschrieben (und mit io.file.readAllTextAsync() wird sie in einem Schritt gelesen). Sie können Textdateien jedoch auch zeilenweise lesen oder schreiben, wie im folgenden Beispiel.

Eine Textdatei zeilenweise lesen:

runtime.handleAsync(async function () {
 
    // Wir möchten die aktuelle CoDaBix-Runtime-Logdatei auslesen.
    const runtimeLogDir = io.path.combinePath(
            runtime.expandEnvironmentVariables("%CodabixDataDir%"), "logfiles");
    const runtimeLogFiles = await io.directory.getFilesAsync(runtimeLogDir);
 
    // Die zurückgegebenen Dateien sind aufsteigend nach ihrem Namen sortiert; daher
    // verwenden wir den letzten Eintrag, um an die heutige Logdatei zu kommen.
    const logFile = io.path.combinePath(runtimeLogDir,
            runtimeLogFiles[runtimeLogFiles.length - 1]);
 
    // Öffne die Datei mit einem FileReader und lies die ersten 5 Zeilen.
    let result = "";
    let reader = await io.file.openFileReaderAsync(logFile);
    try {
        let lines = await reader.readLinesAsync(5);
        for (let i = 0; i < lines.length; i++) {
            // Füge die Zeile an den Result-String an.
            result += "\n[" + (i + 1) + "]: " + lines[i];
        }
    }
    finally {
        // Stelle sicher, dass der Reader geschlossen wird, wenn wir fertig sind.
        await reader.closeAsync();
    }
 
    // Logge den Result-String.
    logger.log(result);
 
} ());

Dieser Code liest die ersten 5 Zeilen der aktuellen CoDaBix-Runtime-Logdatei aus und schreibt diese ins Script-Log:

HTTP-Handler

Der Namespace net bietet Methoden zum Registrieren von HTTP-Handler, womit Sie eine Script-Funktion angeben können, die immer dann aufgerufen wird, wenn ein Client einen HTTP-Request an den CoDaBix-Webserver sendet. Damit können Sie beispielsweise HTML-Seiten, ähnlich wie bei PHP oder ASP.NET, dynamisch generieren. Außerdem können Sie damit auch WebSocket-Verbindungen entgegennehmen (siehe nächstes Kapitel).

Um einen HTTP-Handler zu registrieren, müssen Sie die net.registerHttpRoute()-Methode aufrufen, die als Argumente einen Pfad sowie einen Callback erwartet. Der Callback wird dann aufgerufen, wenn ein HTTP-Request für den Pfad /scripthandlers/<path> eintrifft, wobei <path> den registrierten Pfad darstellt.
Beachten Sie, dass ein Pfad immer nur insgesamt einmal registriert werden kann. Wenn z.B. ein anderes Script bereits einen Handler für den gleichen Pfad registriert hat, wirft die Methode eine Ausnahme.
Beachten Sie außerdem, dass der Pfad immer „roh“, also URL-kodiert angegeben werden muss. Wenn der User beispielsweise im Browser als Adresse /scripthandlers/März eingeben können soll, müssten Sie als Pfad M%C3%A4rz angeben.

Der Callback muss hierbei eine Funktion sein, die ein Promise-Objekt zurückgibt (das passiert automatisch, wenn Sie eine asynchrone Funktion erstellen). Beim Eintreffen eines HTTP-Requests wird diese Funktion dann aufgerufen, und der HTTP-Request bleibt solange aktiv, bis das Promise-Objekt „fulfilled“ ist (also die asynchrone Funktion zurückkehrt). Der Callback enthält als Parameter ein net.HttpContext-Objekt, das die Properties request und response enthält, um entsprechend auf Objekte für die Anfrage (Request) des Clients und die Antwort (Response) an den Client zuzugreifen.

Textseite dynamisch generieren

Nehmen wir an, Sie möchten die aktuelle Uhrzeit sowie die derzeitige Anzahl an CoDaBix-Nodes in einer Textseite ausgeben, wenn der Benutzer im Browser http://localhost:8181/scripthandlers/hello-world auf dem lokalen PC eingibt (vorausgesetzt, der lokale Port für den CoDaBix-Webserver ist auf den Standardwert 8181 eingestellt). Im folgenden Scriptcode wird dazu ein HTTP-Handler für diesen Pfad registriert, der beim Aufruf einen Text mit den entsprechenden Informationen generiert:

runtime.handleAsync(async function () {
 
    net.registerHttpRoute("hello-world", async ctx => {
        let response = ctx.response;
 
        // Erstelle einen Text mit dem aktuellen Datum und der Anzahl an Codabix-Nodes.
        let text = "Hello World! Time: " + new Date().toLocaleTimeString() + "\n\n" +
            "Node Count: " + countNodes(codabix.rootNode.children);
 
        // Setze den Content-Type auf das Textformat, damit der Browser weiß,
        // wie er das Dokument anzeigen soll.
        response.contentType = "text/plain";
 
        // Schließlich schreiben wir den generierten Text in die Response.
        // Wir müssen Exceptions abfangen, da es passieren könnte, dass der
        // Client die Verbindung bereits getrennt hat.
        try {
            await response.writeBodyCompleteAsync(text);
        }
        catch (e) {
            logger.log("Write Exception: " + e);
        }
    });
 
    function countNodes(nodes: codabix.Node[]): number {
        let count = nodes.length;
        for (let node of nodes) {
            count += countNodes(node.children);
        }
        return count;
    }
 
}());

Wenn Sie diese URL nun im Browser aufrufen, erhalten Sie eine Ausgabe ähnlich wie dieser Screenshot zeigt:

Wenn Sie die Seite (mit F5) aktualisieren, sehen Sie dann immer wieder die aktuelle Uhrzeit angezeigt.

Das Script verwendet die Methode writeBodyCompleteAsync() des response-Objektes, um den generierten Text an den Browser zu senden (als Textkodierung wird UTF-8 verwendet). Es wird empfohlen, diese Methode statt writeBodyAsync() aufzurufen, wenn Sie den kompletten Ausgabetext im Script erstellen können. writeBodyAsync() hingegen können Sie verwenden, wenn Sie immer nur kleinere Teile des Response-Texts generieren und sofort an den Client schicken möchten.

Beachten Sie: Sie sollten Schreibvorgänge immer in einem try-catch-Block platzieren, da diese eine Ausnahme auslösen können, wenn der Client beispielsweise die Verbindung bereits getrennt hat.

HTML-Seite mit Eingabeformular generieren

Nehmen wir nun an, Sie möchten eine HTML-Seite erstellen, auf der der Benutzer einen Nodepfad eingeben kann. Wenn er das Formular abschickt, soll der Wert des Nodes in einem synchronen Lesevorgang gelesen und dann ausgegeben werden. Dies wird in folgendem Scriptcode gemacht:

runtime.handleAsync(async function () {
 
    const readNodeHandlerPath = "read-node-value";
    net.registerHttpRoute(readNodeHandlerPath, async ctx => {
        let request = ctx.request;
        let response = ctx.response;
 
        // Erstelle eine HTML-Seite mit einem Formular zum Eingeben
        // eines Node-Pfads. Wenn dieser eingegeben wurde, suchen wir den
        // Node und starten einen synchronen Lesevorgang.
 
        // Nachschauen, ob das Formular bereits abgeschickt wurde. Wenn nicht,
        // schreiben wir einen Beispielpfad in die Textbox.
        const inputNodePathName = "nodePath";
        const inputNodePathValue = request.queryString[inputNodePathName];
        let resultMessage = "";
 
        if (inputNodePathValue != undefined) {
            // Das Formular wurde abgeschickt, daher suchen wir nun den Node heraus.
            let node = codabix.findNode(inputNodePathValue);
            if (!node) {
                // Wir konnten den Node nicht finden.
                resultMessage = `Error! Node with path '${inputNodePathValue}' was not found!`;
            }
            else {
                // Synchronen Lesevorgang ausführen... (Dies könnte etwas dauern, je nach Device)
                // Der HTTP-Request bleibt solange aktiv.
                let resultValue = (await codabix.readNodeValuesAsync(node))[0];
                if (resultValue == null) // Der Node hat noch keinen Wert
                    resultMessage = `Result: Node does not have a value!`;
                else
                    resultMessage = `Value of Node '${inputNodePathValue}': ${resultValue.value} ${node.unit || ""}`;
            }
        }
 
        let html = `<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>Read Node Value</title>
</head>
<body>
    <h1>Read Node Value!</h1>
    <form method="get" action="${xml.encode(readNodeHandlerPath)}">
        Enter Node Path:
        <input type="text" name="${xml.encode(inputNodePathName)}" style="width: 250px;"
            value="${xml.encode(inputNodePathValue == undefined ? "/Nodes/Demo-Nodes/Temperature" : inputNodePathValue)}" />
        <button>OK</button>
    </form>
    <p>
        ${xml.encode(resultMessage)}
    </p>
</body>
</html>`;
 
        // Setze den Content-Type auf HTML, und schreibe das generierte HTML in die Response.
        response.contentType = "text/html";
        try {
            await response.writeBodyCompleteAsync(html);
        }
        catch (e) {
            logger.log("Write Exception: " + e);
        }
    });
 
}());

Beachten Sie: Das Script nutzt die Methode io.util.encodeXml(), um Strings so zu kodieren, dass diese in HTML- oder XML-Text (bzw. Attributwerten) gefahrlos ausgegeben werden können, um damit keine Angriffsfläche für HTML-Injection bzw. Cross-Site-Scripting (XSS) zu bieten. Dies ist besonders dann wichtig, wenn Strings in einer HTML-Seite ausgegeben werden sollen, die vom Benutzer stammen könnten.

Wenn Sie nun die URL http://localhost:8181/scripthandlers/read-node-value im Browser aufrufen, erhalten Sie folgendes Formular angezeigt:


Wenn Sie nun auf den „OK“-Button klicken (und Sie das Demo-Data-Plugin installiert haben), wird der aktuelle Wert der Temperature-Demo-Node angezeigt:



Das Script hat also auf das abgesendete Formular reagiert und den aktuellen Wert des Nodes ausgegeben. Da das Formular die GET-Methode benutzt, ist der Parameter in der URL als angehängter Query-String sichtbar: ?nodePath=%2FNodes%2FDemo-Nodes%2FTemperature

Genauso können Sie nun beispielsweise einen Pfad auf eine S7-Variable, OPC UA Client Variable oder ähnliches eingeben. In diesem Fall wird nicht nur der derzeit gespeicherte Wert der Variablen ausgegeben, sondern durch den Aufruf der codabix.readNodeValuesAsync()-Methode ein synchroner Lesevorgang gestartet, der den Wert vom Device liest und diesen anschließend in die HTML-Seite ausgibt.

Historische Daten als Diagramm anzeigen

Sie können über den HTTP-Handler auch eine SVG-Grafik generieren, die historische Werte als Diagramm anzeigt (im Beispiel vom Gradient-Demonode):

Dies wird über folgenden Scriptcode ermöglicht:

// Utilities for generating a SVG diagram.
namespace SvgLibrary {
    export interface Value {
        date: number,
        value: number
    }
 
    type DPoint = [number, number];
 
    const escapeXml = xml.encode;
 
    /**
     * Formats pixel coordinates. A max. precision of 3 should be good enough here.
     * @param x
     */
    let pform = (x: number): string => (Math.round(x * 1000) / 1000).toString();
 
    function determineGap(x: number): number {
        let sign = x < 0 ? -1 : 1;
        x = Math.abs(x);
        let tenLog = Math.floor(Math.log10(x));
 
        if (x > 5 * 10 ** tenLog)
            x = 10 * 10 ** tenLog;
        else if (x > 2 * 10 ** tenLog)
            x = 5 * 10 ** tenLog;
        else if (x > 1 * 10 ** tenLog)
            x = 2 * 10 ** tenLog;
        else
            x = 1 * 10 ** tenLog;
 
        return x * sign;
    }
 
    function formatTwoDigits(values: number[], joinStr: string): string {
        let outStr = "";
        for (let i = 0; i < values.length; i++) {
            if (i > 0)
                outStr += joinStr;
            let str = values[i].toString();
            while (str.length < 2)
                str = "0" + str;
            outStr += str;
        }
        return outStr;
    }
 
    export function generateValueChartSvg(values: Value[], width: number, height: number, 
        minValue?: number, maxValue?: number, useStairway = false): string {
 
        // Ensure the array isn't empty.
        if (values.length == 0)
            throw new Error("No history values available.");
 
        let outStr = "";
 
        // Determine the maximum and minimum values.
        // Ensure the values are ordered by date.
        values.sort((a, b) => a.date - b.date);
 
        let minV: number | null = null, maxV: number | null = null;
        let minD = values[0].date;
        let maxD = values[values.length - 1].date;
        for (let n of values) {
            minV = minV === null ? n.value : Math.min(minV, n.value);
            maxV = maxV === null ? n.value : Math.max(maxV, n.value);
        }
        if (minV == null || maxV == null)
            throw new Error("Could not determine min/max");
 
        // Ensure that if all values are the same we don't get Infinity/NaN.
        if (maxV - minV < 0.00001 * minV)
            maxV = minV + 0.00001 * minV, minV = minV - 0.00001 * minV;
 
        const padding = 30;
        let yTop = maxV + (maxV - minV) / (height - 2 * padding) * padding; // 20 pixel padding
        let yBottom = minV - (maxV - minV) / (height - 2 * padding) * padding;
        let xLeft = minD - (maxD - minD) / (width - 2 * padding) * padding;
        let xRight = maxD + (maxD - minD) / (width - 2 * padding) * padding;
 
        let convCoords = (coords: DPoint): DPoint =>
            [(coords[0] - xLeft) / (xRight - xLeft) * width,
                (yTop - coords[1]) / (yTop - yBottom) * height];
 
        // Create the svg and draw the points.
        outStr += `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" baseProfile="full" width="${width}px" height="${height}px" viewBox="0 0 ${width} ${height}">`;
 
        let convMax = maxValue == null ? null : convCoords([0, maxValue]);
        let convMin = minValue == null ? null : convCoords([0, minValue]);
        // Draw rects for the min and max values.
        if (convMax != null && convMax[1] >= 0)
            outStr += `<rect x="0" y="0" width="${width}" height="${pform(convMax[1])}" fill="#fcdada"/>`;
        if (convMin != null && convMin[1] < height)
            outStr += `<rect x="0" y="${pform(convMin[1])}" width="${width}" height="${pform(height - convMin[1])}" fill="#fcdada"/>`;
 
        // Draw a line for the x and y axis. We simply draw it at the bottom / left.
        let conv1 = convCoords([minD, minV]);
        let conv2 = convCoords([maxD, minV]);
        outStr += `<line x1="${escapeXml(pform(conv1[0]))}" y1="${escapeXml(pform(conv1[1]))}" x2="${escapeXml(pform(conv2[0]))}" y2="${escapeXml(pform(conv2[1]))}" stroke="grey" stroke-width="1"/>`;
        conv1 = convCoords([minD, maxV]);
        conv2 = convCoords([minD, minV]);
        outStr += `<line x1="${escapeXml(pform(conv1[0]))}" y1="${escapeXml(pform(conv1[1]))}" x2="${escapeXml(pform(conv2[0]))}" y2="${escapeXml(pform(conv2[1]))}" stroke="grey" stroke-width="1"/>`;
 
        // Now draw some small lines which indicates the continuous x and y values.
        // We initially use 25 pixel then get the smallest number of 1*10^n, 2*10^n or 5*10^n that is >= our number.
        const fontSize = 12;
        let xLineGap = determineGap(40 / (width - 2 * padding) * (xRight - xLeft));
        let xStart = Math.floor(minD / xLineGap + 1) * xLineGap;
        let yLineGap = determineGap(20 / (height - 2 * padding) * (yTop - yBottom));
        let yStart = Math.floor(minV / yLineGap + 1) * yLineGap;
        for (let x = xStart; x <= maxD; x += xLineGap) {
            let conv1 = convCoords([x, minV]);
            let conv2 = [conv1[0], conv1[1] - 10];
            outStr += `<line x1="${escapeXml(pform(conv1[0]))}" y1="${escapeXml(pform(conv1[1]))}" x2="${escapeXml(pform(conv2[0]))}" y2="${escapeXml(pform(conv2[1]))}" stroke="grey" stroke-width="1"/>`;
            let xDate = new Date(x);
            let textContent1 = formatTwoDigits([xDate.getDate(), xDate.getMonth() + 1, xDate.getFullYear() % 100], ".");
            let textContent2 = formatTwoDigits([xDate.getHours(), xDate.getMinutes(), xDate.getSeconds()], ":");
            outStr += `<text x="${escapeXml(pform(conv1[0] - textContent1.length * fontSize / 4))}" y="${escapeXml(pform(conv1[1] + 14))}" style="font-family: Consolas, monospace; font-size: ${fontSize}px;">${escapeXml(textContent1)}</text>`;
            outStr += `<text x="${escapeXml(pform(conv1[0] - textContent2.length * fontSize / 4))}" y="${escapeXml(pform(conv1[1] + 14 + fontSize))}" style="font-family: Consolas, monospace; font-size: ${fontSize}px;">${escapeXml(textContent2)}</text>`;
        }
        for (let y = yStart; y <= maxV; y += yLineGap) {
            let conv1 = convCoords([minD, y]);
            let conv2 = [conv1[0] + 10, conv1[1]];
            outStr += `<line x1="${escapeXml(pform(conv1[0]))}" y1="${escapeXml(pform(conv1[1]))}" x2="${escapeXml(pform(conv2[0]))}" y2="${escapeXml(pform(conv2[1]))}" stroke="grey" stroke-width="1"/>`;
            let textContent = y.toString();
            outStr += `<text x="${escapeXml(pform(conv1[0] - 8 - textContent.length * fontSize / 2))}" y="${escapeXml(pform(conv1[1] + (fontSize / 16 * 10) / 2))}" style="font-family: Consolas, monospace; font-size: ${fontSize}px;">${escapeXml(textContent)}</text>`;
        }    
 
        // Draw the points.
        let pointList = "";
		let prevPoint: DPoint | null = null;
        for (let i = 0; i < values.length; i++) {
            let convPoint = convCoords([values[i].date, values[i].value]);
			if (useStairway && prevPoint !== null) {
				// Use the previous y coordinate with the current x coordinate.
				pointList += `${pform(convPoint[0])},${pform(prevPoint[1])} `;
			}
            pointList += `${pform(convPoint[0])},${pform(convPoint[1])} `;
			prevPoint = convPoint;
        }
        outStr += `<polyline fill="none" stroke="blue" stroke-width="2" points="${escapeXml(pointList)}"/>`;
        outStr += "</svg>";
        return outStr;
    }
}
 
runtime.handleAsync(async function () {
    const gradientNode = codabix.findNode("/Nodes/Demo-Nodes/Gradient", true);
 
    net.registerHttpRoute("graph", async ctx => {
        const response = ctx.response;
 
        // Lies die letzten 50 historischen Werte des Gradient-Demo-Nodes aus und stelle
        // sie dann in einem SVG-Diagramm dar.
        let historyValues = await codabix.readNodeHistoryValuesAsync(gradientNode, null, null, 30);
        let svgValues: SvgLibrary.Value[] = [];
        for (let historyValue of historyValues) {
            if (typeof historyValue.value != "number")
                throw new Error("Expected a number value");
 
            svgValues.push({
                value: historyValue.value as number,
                date: historyValue.timestamp.getTime()
            });
        }
 
        // Generiere das SVG-Diagramm.
        let resultString: string;
        try {
            resultString = SvgLibrary.generateValueChartSvg(svgValues, 700, 400, undefined, 40);
                // Setze den Content-Type auf SVG.
            response.contentType = "image/svg+xml";
        }
        catch (e) {
            // Gib den Fehler aus.
            resultString = String(e);
            response.statusCode = 500;
            response.contentType = "text/plain";
        }
 
        try {
            await response.writeBodyCompleteAsync(resultString);
        }
        catch (e) {
            logger.log("Write Exception: " + e);
        }
    });
 
}());

Erweiterte HTTP-Programmierung

Die bisher gezeigten Beispiele generieren eine HTML-Seite oder SVG-Grafik, die im Browser angezeigt wird, sich danach jedoch nicht mehr verändert. Um dynamische HTML5-Apps zu erstellen, können Sie jedoch z.B. auch eine statische HTML-Seite mit einem JavaScript/TypeScript erstellen, das wiederum mit dem Script auf CoDaBix-Seite kommuniziert, beispielsweise über JSON (zum Serialisieren und Deserialisieren von JSON können Sie JSON.stringify() und JSON.parse() verwenden). Dadurch kann das Script im Browser regelmäßig neue Informationen vom Script in CoDaBix abfragen.

Im nächsten Kapitel werden WebSocket-Verbindungen beschrieben, mit denen Ihre HTML-Seite über eine bidirektionale, socketähnliche Verbindung mit dem CoDaBix-Script kommunizieren kann.

WebSocket-Verbindungen im HTTP-Handler

Sie können in einem Script auch serverseitige WebSocket-Verbindungen betreiben, die z.B. von einem Browser-Script über die dort verfügbare WebSocket-Klasse hergestellt werden. WebSocket erlaubt es, in beiden Richtungen (Server an Client sowie Client an Server) jederzeit neue Daten zu senden bzw. empfangen, solange die Verbindung besteht; wohingegen bei HTTP-Requests der Client ständig neue Requests ausführen müsste, um nachzusehen, ob der Server neue Informationen hat („Polling“). So könnten Sie beispielsweise in einem CoDaBix-Script einen Eventlistener für ein ValueChanged-Event eines Nodes hinzufügen, und immer wenn ein neuer Nodewert geschrieben wurde, diesen per WebSocket-Verbindung an alle verbundene Browser zur Darstellung senden.

Um eine WebSocket-Verbindung entgegenzunehmen, prüfen Sie zuerst mit ctx.isWebSocketRequest, ob es sich tatsächlich um einen WebSocket-Request handelt. Falls ja, rufen Sie ctx.acceptWebSocketAsync() auf, das ein net.RawWebSocket-Objekt zurückgibt. Auf diesem Objekt können Sie nun mit receiveAsync() Messages empfangen und mit sendAsync() Messages senden. Das von diesen Methoden zurückgegebene Promise-Objekt wird dabei erst „fulfilled“, wenn der Empfangs-/Sendevorgang abgeschlossen ist. Falls ein Fehler beim Empfangen oder Senden auftritt (z.B. weil die Verbindung bereits geschlossen wurde), werfen die Methoden eine Ausnahme.

Beachten Sie: Sie können auf einem einzelnen RawWebSocket-Objekt gleichzeitig eine Nachricht empfangen und auch senden (es kann also gleichzeitig jeweils ein Aufruf der receiveAsync()-Methode und der sendAsync()-Methode ausstehend sein).

Für die folgenden WebSocket-Beispiele verwenden wir eine statische HTML-Seite, die eine WebSocket-Verbindung zu CoDaBix aufbaut. Speichern Sie daher bitte zuerst folgende Datei CodabixWebsocket.html im webfiles-Ordner in Ihrem Datenverzeichnis:

CodabixWebsocket.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>CoDaBix WebSocket Example</title>
    <style>
        #connect-container {
            float: left;
            width: 400px
        }
 
        #connect-container div {
            padding: 5px;
        }
 
        #console-container {
            float: left;
            margin-left: 15px;
            width: 400px;
        }
 
        #console {
            border: 1px solid #CCCCCC;
            border-right-color: #999999;
            border-bottom-color: #999999;
            height: 170px;
            overflow-y: scroll;
            padding: 5px;
            width: 100%;
            white-space: pre-wrap;
        }
 
        #console p {
            padding: 0;
            margin: 0;
        }
    </style>
    <script>
"use strict";
document.addEventListener("DOMContentLoaded", function() {
 
    var ws = null;
 
    var connectButton = document.getElementById('connect');
    var disconnectButton = document.getElementById('disconnect');
    var echoButton = document.getElementById('echo');
    var messageBox = document.getElementById('message');
    var consoleArea = document.getElementById('console');
 
    function setConnected(connected, fullyConnected) {
        connectButton.disabled = connected;
        disconnectButton.disabled = !connected;
        echoButton.disabled = !(connected && fullyConnected);
    }
    setConnected(false);
 
    function connect() {
        if (ws != null)
            return;
 
        if (typeof WebSocket == "undefined")
            alert("WebSocket is not supported by this browser.");
 
        var wsUrl = (window.location.protocol == "https:" ? "wss:" : "ws:") + '//' +
            window.location.host + "/scripthandlers/websocket";
        var localWs = ws = new WebSocket(wsUrl);
        setConnected(true);
        log("Info: WebSocket connecting...");
 
        ws.onopen = function () {
            if (localWs != ws)
                return;
 
            setConnected(true, true);
            log("Info: WebSocket connection opened.");
        };
        ws.onmessage = function (event) {
            if (localWs != ws)
                return;
 
            log("Received: " + event.data);
        };
        ws.onclose = function (event) {
            if (localWs != ws)
                return;
 
            setConnected(false);
            log("Info: WebSocket connection closed, Code: " + event.code +
                (event.reason == "" ? "" : ", Reason: " + event.reason));
            ws = null;
        };
    }
 
    function disconnect() {
        if (ws == null)
            return;
 
        setConnected(false);
        log("Info: WebSocket connection closed by user.");
        ws.close();
        ws = null;        
    }
 
    function sendText() {
        if (ws == null)
            return;
 
        var message = messageBox.value;
        ws.send(message);
        log("Sent: " + message);        
    }
 
    function log(message) {
        var p = document.createElement('p');
        p.style.wordWrap = 'break-word';
        p.appendChild(document.createTextNode(message));
        consoleArea.appendChild(p);
        while (consoleArea.childNodes.length > 25) {
            consoleArea.removeChild(consoleArea.firstChild);
        }
        consoleArea.scrollTop = consoleArea.scrollHeight;
    }
 
    // Add event listeners to the buttons.
    connectButton.onclick = function() {
        connect();
    };
 
    disconnectButton.onclick = function() {
        disconnect();
    };
 
    echoButton.onclick = function() {
        sendText();
    };
 
}, false);
    </script>
</head>
<body>
<div>
    <div id="connect-container">
        <div>
            <button id="connect">Connect</button>
            <button id="disconnect">Disconnect</button>
        </div>
        <div>
            <textarea id="message" style="width: 350px">Hello world!</textarea>
        </div>
        <div>
            <button id="echo">Send Message</button>
        </div>
    </div>
    <div id="console-container">
        <div id="console"></div>
    </div>
</div>
</body>
</html>

Sie sollten diese Seite nun aufrufen können, wenn Sie im Browser http://localhost:8181/webfiles/CodabixWebsocket.html aufrufen (vorausgesetzt, der lokale Port für den CoDaBix-Webserver ist auf den Standardwert 8181 eingestellt und die Option „Serve Static Web Files“ in den CoDaBix Einstellungen wurde nicht deaktiviert):

Einfaches Echo-WebSocket

Im einfachsten Fall können Sie Nachrichten von einem WebSocket empfangen, und diese direkt wieder an den Client zurücksenden, wie das folgende CoDaBix-Script zeigt:

runtime.handleAsync(async function () {
 
    const maxMessageLength = 10000;
 
    net.registerHttpRoute("websocket", async ctx => {
        // Check if the client sent a WebSocket request.
        if (!ctx.isWebSocketRequest) {
            ctx.response.statusCode = 400;
        }
        else {
            try {
                // Accept the WebSocket request.
                let ws = await ctx.acceptWebSocketAsync();
 
                // In a loop, receive messages from the client and send them back.
                // Once the client closes the connection, receive() will return null
                // or throw an exception.
                while (true) {
                    let message = await ws.receiveAsync(maxMessageLength);
                    if (message == null || message.length == maxMessageLength)
                        break; // connection closed or message too large
 
                    logger.log("Received message: " + message);
 
                    // Send the message back to the client.
                    await ws.sendAsync("Hello from CoDaBix: " + message);
                }
            }
            catch (e) {
                logger.log("Error: " + e);
            }
        }
    });
 
}());

Der HTTP-Handler nimmt eine WebSocket-Verbindung entgegen, und liest dann immer wieder Nachrichten vom Client und sendet diese zurück. Dies passiert solange, bis der Client die Verbindung trennt.

In der HTML-Seite können Sie nun mit „Connect“ eine Verbindung herstellen und Nachrichten an CoDaBix senden, die dann sofort zurückkommen sollten:

"Chat" mit Hintergrund-Sendeoperationen

Nehmen wir nun an, Sie möchten über WebSocket allen verbundenen Benutzern jeweils sofort den aktuellen Wert des Temperatur-Demonodes senden, sobald dieser in CoDaBix geschrieben wird. Zusätzlich sollen die Benutzer untereinander chatten können.

Da die sendAsync()-Methode des WebSockets so lange (asynchron) blockiert, bis die Daten vollständig gesendet wurden, können wir diese nicht einfach aufrufen, wenn eine neuer Temperaturwert oder eine Chatnachricht gesendet werden soll, da möglicherweise die vorherige Nachricht noch nicht vollständig an den Client gesendet wurde. Stattdessen verwenden wir eine Queue, in der die zu sendenden Nachrichten abgelegt werden. Eine andere Scriptfunktion nimmt dann jeweils immer eine Nachricht aus dieser Queue und sendet diese über das WebSocket. Sobald der Sendevorgang abgeschlossen ist, nimmt sie die nächste Nachricht aus der Queue, oder wartet bis wieder mindestens eine Nachricht in der Queue ist.

Im folgenden Scriptcode erledigt dies die Klasse WebSocketSender, die Sie auch für andere Scripts wiederverwenden können.

(Bitte deaktivieren Sie vorher das zuvor beschriebene Echo-WebSocket-Script, falls Sie dieses ausführen, da beide Scripte den gleichen HTTP-Path registrieren würden.)

runtime.handleAsync(async function () {
 
    /**
     * A utility class that implements a Send Queue for WebSockets. This allows you to
     * push messages to be sent to the client to the queue without having to wait until
     * the client has received it.
     */
    class WebSocketSender {
        // The queued messages to be sent to the client.
        private sendQueue: string[] = [];
        // The callback to resolve the promise of the sender function.
        private sendResolver: (() => void) | null = null;
        private senderIsExited = false;
 
        constructor(private ws: net.RawWebSocket) {
            // Start the sender function which will then wait for the next message queue entry.
            runtime.handleAsync(this.runSenderAsync());
        }
 
        /**
         * Adds the specified message to the send queue.
         */
        public send(s: string) {
            // If the sender already exited due to an error, do nothing.
            if (this.senderIsExited)
                return;
 
            // TODO: Check if the number of buffered messages exceeds a specific limit.
            this.sendQueue.push(s);
 
            // If the sender waits for the next queue entry, release the sender.
            if (this.sendResolver) {
                this.sendResolver();
                this.sendResolver = null;
            }
        }
 
        private async runSenderAsync() {
            try {
                // In a loop, we will wait until the next send queue entry arrives.
                while (true) {
                    for (let i = 0; i < this.sendQueue.length; i++) {
                        await this.ws.sendAsync(this.sendQueue[i]);
                    }
                    this.sendQueue = [];
 
                    // Create a Promise and cache the resolve handler, so that the
                    // send() method can fulfill the Promise later.
                    await new Promise(resolve => this.sendResolver = resolve);
                }
            }
            catch (e) {
                // An error occured, e.g. when the client closed the connection.
                // This means the receive handler should also get an exception when it
                // tries to receive the next message from the client.
                logger.log("Send Error: " + e);
            }
            finally {
                this.senderIsExited = true;
            }
        }
    }
 
 
    // The maximum message size which we will receive.
    const maxReceiveLength = 10000;
 
    interface ChatUser {
        name: string;
        sender: WebSocketSender;
    }
 
    // The set of currently connected users.
    const chatUsers = new Set<ChatUser>();
    let currentUserId = 0;
 
 
    // Register for the ValueChanged event of the "Temperature" node. Every time a
    // new value occurs, we will broadcast it to all connected users.
    const temperatureNode = codabix.findNode("/Nodes/Demo-Nodes/Temperature", true);       
 
    const getTemperatureValueMessage = function (value: codabix.NodeValue | null) {
        return `Temperature: ${value && value.value} ${temperatureNode.unit || ""}`;
    };
 
    temperatureNode.addValueChangedEventListener(e => {
        // Send the new value to all connected users.
        const message = getTemperatureValueMessage(e.newValue);
        chatUsers.forEach(u => {
            u.sender.send(message);
        });
    });
 
 
    // Register the websocket handler.
    net.registerHttpRoute("websocket", async ctx => {
        // Check if the client sent a WebSocket request.
        if (!ctx.isWebSocketRequest) {
            ctx.response.statusCode = 400;
        }
        else {
            // Accept the WebSocket request.
            let ws: net.RawWebSocket;
            try {
                ws = await ctx.acceptWebSocketAsync();
            }
            catch (e) {
                logger.log("Accept Error: " + e);
                return;
            }
 
            await handleWebSocketAsync(ws);
        }
    });
 
    // The function that handles the WebSocket connection.
    const handleWebSocketAsync = async function (ws: net.RawWebSocket) {
        // Create a send queue which we use to send messages to the client.
        let sender = new WebSocketSender(ws);
 
        // Generate a new user name, then notify the existing users
        // that a new user joined.
        let user = {
            name: "User-" + ++currentUserId,
            sender: sender
        };
        chatUsers.add(user);
 
        try {
            const userJoinedMessage = `${user.name} joined.`;
            chatUsers.forEach(u => {
                u.sender.send(userJoinedMessage);
            });
 
            // Send the initial temperature value to the new user.
            user.sender.send(getTemperatureValueMessage(temperatureNode.value));
 
            // Now start to receive the messages from the client.
            while (true) {
                let receivedMessage: string | null;
                try {
                    receivedMessage = await ws.receiveAsync(maxReceiveLength);
                    if (receivedMessage == null || receivedMessage.length >= maxReceiveLength)
                        break; // connection closed or message too large
                }
                catch (e) {
                    logger.log("Receive Error: " + e);
                    break;
                }
 
                // Broadcast the received message.
                const broadcastMessage = `${user.name}: ` + receivedMessage;
                chatUsers.forEach(u => {
                    u.sender.send(broadcastMessage);
                });
            }
        }
        finally {
            // The client has closed the connection, or there was an error when receiving.
            // Therefore, we notify the other users that the current user has left.
            chatUsers.delete(user);
 
            const userLeftMessage = `${user.name} left.`;
            chatUsers.forEach(u => {
                u.sender.send(userLeftMessage);
            });
        }
    }
 
}());

Sobald sich ein Benutzer verbindet, wird diesem ein Benutzername zugewiesen (z.B. User-6) und eine Nachricht an alle verbundenen Benutzer geschickt. Der neue Benutzer erhält auch den aktuellen Wert des Temperatur-Nodes. Sobald dann immer ein neuer Wert für die Temperatur geschrieben wurde oder ein anderer Benutzer eine Chatnachricht sendet, wird diese an alle verbundenen Benutzer (praktisch ein Broadcast) geschickt:

Senden von HTTP-Requests

Der Namespace net.httpClient stellt Methoden zur Verfügung, die es Ihnen ermöglichen, HTTP-Requests an andere Server zu senden. So können Sie beispielsweise externe REST-APIs über JSON-Objekte ansprechen.

Wichtig: Beim Senden von Requests über das Internet (oder über andere potentiell unsichere Netzwerke), stellen Sie bitte sicher, dass Sie falls möglich immer https:-URLs verwenden, da http:-URLs eine unsichere Verbindung verwenden und deshalb die Authentizität des Servers sowie die Vertraulichkeit und Integrität der Daten nicht gewährleistet ist. Dies gilt vor allem dann, wenn der Request vertrauliche Zugangsdaten enthält.

Beachten Sie: Derzeit können Request- und Response-Bodies nur als Textdaten, nicht als Binärdaten verwendet werden.

Einfache GET-Requests

Der folgende Code führt einen einfachen GET-Request aus und loggt den Response-Body (falls vorhanden):

    let response = await net.httpClient.sendAsync({
        url: "https://www.codabix.com/en/start"
    });
 
    if (response.content) {
        logger.log("Result: " + response.content.body);
    }

Zugreifen auf eine JSON-bsierte REST-API

Wenn Sie auf eine URL zugreifen möchten, die ein JSON-Ergebnis liefert, können Sie JSON.parse() verwenden, um den JSON-Response-Body-String in ein JavaScript-Objekt zu konvertieren und auf dieses zuzugreifen.

Das folgende Beispiel greift auf eine externe REST-API zu, um Wetterdaten (Temperatur) in einem regelmäßigen Intervall abzufragen und in die /Nodes/Temperature-Node zu schreiben:

    let temperatureNode = codabix.findNode("/Nodes/Temperature", true);
 
    while (true) {
        let valueToWrite: codabix.NodeValue;
        try {
            let response = await net.httpClient.sendAsync({
                // TODO: Use "https:" when the server supports this,
                // to guarantee data authenticity and integrity
                url: "http://api.openmeteo.com/observations/openmeteo/1001/t2"
            });
 
            let result = JSON.parse(response.content!.body);
            valueToWrite = new codabix.NodeValue(result[1]);
        }
        catch (e) {
            logger.logWarning("Could not retrieve weather data: " + e);
            valueToWrite = new codabix.NodeValue(null, undefined, {
                statusCode: codabix.NodeValueStatusCodeEnum.Bad,
                statusText: String(e)
            });
        }
 
        // Write the temperature value.
        await codabix.writeNodeValueAsync(temperatureNode, valueToWrite);
 
        // Wait 5 seconds
        await timer.delayAsync(5000);
    }

Das folgende Beispiel zeigt, wie ein POST-Request zu der CoDaBix-internen REST-API gesendet werden kann (für <encrypted-password> müssten Sie das verschlüsselte Benutzerpasswort angeben):

    let result = await net.httpClient.sendAsync({
        // Note: For external servers you should "https:" for a secure connection.
        url: "http://localhost:8181/api/json",
        method: "POST",
        content: {
            headers: {
                "content-type": "application/json"
            },
            body: JSON.stringify({
                username: "demo@user.org",
                password: codabix.security.decryptPassword("<encrypted-password>"),
                browse: {
                    na: "/Nodes"
                }
            })
        }
    });
 
    let jsonResult = JSON.parse(result.content!.body);
    // TODO: Process JSON result...
Die folgende Liste gibt einige Empfehlungen für performante und saubere Scripts:
  • Falls Sie kein Library-Script schreiben, platzieren Sie Ihren gesamten Code innerhalb einer IIFE (Immediately-Invoked Function Expression) wie folgt:
    Blank Template.js
    runtime.handleAsync(async function () {
     
        // Ihr Code...
     
    } ());

    Dies verhindert das „Verschmutzen“ des globalen Gültigkeitsbereichs, und der TypeScript-Compiler kann nicht verwendete lokale Variablen erkennen und beschwert sich nicht über das Verwenden von anonymen Typen in exportierten Variablen.
    Wir empfehlen, die Funktion mit dem async-Attribut zu markieren, damit Sie asynchrone Funktionen unter Verwendung des await-Keywords aufrufen können. In diesem Fall sollten Sie wie im obigen Beispiel das zurückgegebene Promise-Objekt an runtime.handleAsync() übergeben, um sicherzustellen, dass unbehandelte Ausnahmen nicht lautlos „verschluckt“ werden.

  • Verwenden Sie immer let oder const zum Deklarieren von Variablen, nicht var, da letzteres die Variable nicht im Gültigkeitsbereich des lokalen Blocks erzeugt, sondern der gesamten Funktion.
  • Anstelle von arguments, welches ein „magisches“, arrayähnliches Objekt ist, benutzen Sie Rest Parameters (...args), welche eine echte Array-Instanz sind und die vorhergehenden Funktionsparameter nicht enthalten.
  • Wenn Sie ein Array enumerieren möchten, benutzen Sie nicht for-in - verwenden Sie stattdessen for-of, da ersteres keine bestimmte Auflistungsreihenfolge garantiert.
  • Wenn Sie eine Ausnahme auslösen, verwenden Sie keine primitiven Werte wie bei throw "Meine Fehlermeldung.";. Erstellen Sie stattdessen eine Instanz des Error-Objekts und verwenden Sie dieses für die throw-Anweisung, wie bei throw new Error("Meine Fehlermeldung");. Dadurch ist sichergestellt, dass Sie eine Stapelnachverfolgung der Stelle abrufen können, an der die Ausnahme ausgelöst wurde.
  • Falls möglich, vermeiden Sie es, Closures in Code zu erstellen, der sehr oft ausgeführt wird (z.B. in einer Schleife), da das Erstellen von Closures in JavaScript teuer ist. Prüfen Sie stattdessen, ob Sie eine Funktion durch das Übergeben von Argumenten wiederverwenden können.
    Angenommen, Sie möchten das ValueChanged-Event eines Nodes behandeln und dort einen neuen Wert schreiben (wofür codabix.scheduleCallback() verwendet werden muss), vermeiden Sie folgenden Code, der eine jedes Mal, wenn das Event aufgerufen wird, eine Closure erstellt:
    myNode.addValueChangedEventListener(e => codabix.scheduleCallback(() => {
        void codabix.writeNodeValueAsync(otherNode, e.newValue);
    }));

    Erstellen Sie stattdessen einmalig eine Closure und rufen dann im Eventlistener die folgende Variante von codabix.scheduleCallback() auf, bei der Argumente weitergegeben werden können:

    const scheduledHandler = (newVal: codabix.NodeValue) => {
        void codabix.writeNodeValueAsync(otherNode, newVal);
    };
    myNode.addValueChangedEventListener(e => codabix.scheduleCallback(scheduledHandler, e.newValue));


Passwörter sicher ablegen

In einigen Fällen müssen Sie Passwörter im Scriptcode ablegen, um beispielsweise auf externe passwortgeschützte HTTP-Dienste zuzugreifen. Damit ein Passwort nicht im Klartext abgelegt werden muss, können Sie es zuvor einmalig über die CoDaBix Webkonfiguration im Menüpunkt „Password Security“ verschlüsseln, und anschließend im Scriptcode am Anfang das verschlüsselte Passwort über einen Aufruf von codabix.security.decryptPassword() entschlüsseln. Das Passwort wird dadurch mit dem Password Key der Backend-Datenbank verschlüsselt, der beim Anlegen der Datenbank unter Verwendung eines kryptographisch sicheren Zufallszahlengenerators erstellt wurde.
Die gleiche Vorgehensweise wird auch beim Speichern von Passwörtern in Datenpunktnodes vom Typ „Password“ verwendet, wie es beispielsweise bei Device-Plugins der Fall ist.

Dies bietet Ihnen zusätzlichen Schutz:

  • Passwörter werden nicht im Klartext angezeigt, daher können Sie den Scriptcode gefahrlos anzeigen, ohne dass andere Personen das Passwort direkt ablesen können.
  • Wenn Sie beim Erstellen eines Backups die Option Include Password Key deaktivieren, können die Passwörter nicht mehr aus dem Backup extrahiert (entschlüsselt) werden.

Beachten Sie: In den CoDaBix Settings kann ein neuer Password Key generiert werden, wodurch Passwörter, die mit dem alten Key verschlüsselt wurden, nicht mehr lesbar sind.


Wenn Sie beipielsweise das Passwort „test“ im Scriptcode verwenden möchten, gehen Sie in der CoDaBix Webkonfiguration auf „Password Security“ und geben dort das Passwort ein. Nach einem Klick auf „Encrypt“ wird das verschlüsselte Passwort ausgegeben.

Beispiel:

Das verschlüsselte Passwort können Sie nun kopieren und anschließend in dem Script einbauen, in dem Sie das Passwort benötigen:

// Entschlüssle das Passwort, das wir für den HTTP-Request benötigen.
const myPassword = codabix.security.decryptPassword(
    "K8D/VWnVQG45HSFlD1G94RwXzrSxH3ARlzzhHekoAdf+prcwe5y6S4FhPUug1Ycw#");
 
// ...
 
function doRequest() {
    let request = {
        user: "myUser",
        password: myPassword
    }
    // TODO: Sende den Request...
}