TypeScript Entwicklungsworkflow

Seit ein paar Tagen stelle ich meinen Entwicklungsworkflow für HTML5-Applikationen um. Ich nutze jetzt viel mehr TypeScript und möchte mir das Entwickeln damit natürlich erleichtern. Zum Glück bietet der TypeScript Compiler einige Features, die einem dabei helfen.

Zu aller erst:

~$ tsc --init

Mit diesem Befehl wird eine Datei mit dem Namen tsconfig.json angelegt. Nach dieser Datei sucht der TypeScript Compiler automatisch und führt den darin beschriebenen Kompiliervorgang aus.

~$ tsc -w

Dieser Parameter veranlasst tsc dazu, das Verzeichnis zu überwachen und bei jeder gespeicherten Änderung die Datei(en) neu zu kompilieren.

~$ tsc -d

TSC bietet außerdem die Möglichkeit aus dem Projekt Definitionen zu erzeugen, damit fremde Programmierer Statement Completion nutzen können, ohne die TypeScript-Datei zu laden.

Visual Studio Code und eigene TypeScript Definition Files

Vor kurzem bin ich über Visual Studio Code gestolpert und bin angenehm überrascht. Ich hätte nicht gedacht, dass Microsoft es wirklich schafft, die Annehmlichkeiten einer großen IDE in einen leichtgewichtigen Editor zu verpacken.

Nun habe ich begonnen VSCode auch im Alltag einzusetzen. Ich bin damit deutlich schneller unterwegs als mit Brackets. In den letzten Tagen habe ich meine Vertretungsplan-Webapp erweitert und wollte jetzt für meine Vertretungsplan-Bibliothek, die ich geschrieben habe, TypeScript Definition Files anlegen. Mit denen kann VSCode die IntelliSense-Dienste anbieten.

Nach einigem hin und her und ganz viel Gesuche habe ich endlich die Lösung des Rätsels gefunden. Und ich musste einen Fehler akzeptieren.
Das alte Resultat ist folgendes:

interface VP {
    new (webViewElementID: string): VP;
    prototype: VP;
     
    username: string;
    password: string;
    webView: string;
    classID: number;
    CW: number;
    genericPlanStart: string;
    genericTeacherPlanStart: string;
    classList: string[];
    teacherList: string[];
    roomList: string[];
    teacherMode: boolean;
     
    setType(type: string): void;
    navigate(): void;
    getCurrentCW(): number;
    retrieveClassList(): void;
    parseRawData(newRawData: string): void;
    parseTeacherRawData(newRawData: string): void;
    getClassList(listType: string): string[];
    setClassID(id: number): void;
    parseClassID(): string;
}
 
declare var Vertretungsplan: VP;
 
declare var classFavList: string[];
declare var classList: string[];
declare var isFaved: boolean; 
declare var isAlreadyFav: boolean;
declare var classID: number;
 
declare function refreshFavListSelector(): void;

Hier die d.ts-Datei für lose Typen-Definitionen:

declare var classFavList: string[];
declare var classList: string[];
declare var isFaved: boolean; 
declare var isAlreadyFav: boolean;
declare var classID: number;

declare function refreshFavListSelector(): void;
declare function getWeekNumber(): number;
declare class VertretungsplanTP {
    webView: string;
    classID: number;
    CW: number;
    currentType: string;
    genericPlanStart: string;
    genericTeacherPlanStart: string;
    bigPlanEnding: string;
    classList: string[];
    teacherList: string[];
    roomList: string[];
    teacherMode: boolean;
    username: string;
    password: string;
    constructor(webViewElementID: string);
    setType(type: string): void;
    navigate(): void;
    getCurrentCW(): number;
    retrieveClassList(): void;
    parseRawData(newRawData: any): void;
    parseTeacherRawData(newRawData: string): void;
    getClassList(listType: string): void;
    setClassID(id: number): void;
    parseClassID(): string;
}

Mit dieser Datei bot mir VSCode endlich IntelliSense-Dienste für meine Bibliothek an. Wenn man seine Bibliothek nicht gerade selber mit TypeScript schreibt und danach kompilieren lässt, dann ist das hier eine nützliche Methode, um IntelliSense-Unterstützung für eigene JS-Dateien zu bekommen.

Eventuell werde ich die Klasse in TypeScript neu implementieren, das ist aber grundsätzlich nicht nötig.


Nachtrag: Ich habe nun die Klasse in TypeScript neu implementiert und habe die Definitionen nun in zwei d.ts-Dateien aufgeteilt. Eine Definition beinhaltet lose Funktionen und Variablen innerhalb der App und die andere wird mit tsc generiert und beinhaltet die Deklarationen und Typen-Definitionen der Vetretungsplan-Klasse.

OpenShift – Begrenzt kostenlose App Engine

Auf meinen Streifzügen durch das Internet bin ich auf OpenShift gestoßen. Dieser Service bietet als kostenlosen Service drei Applikationen mit 1GB Speicher pro Applikation an. Ich rede hier von http://openshift.com.

Unter den nutzbaren Engines findet man PHP, node.js und Python. Ich habe mir erlaubt, eine node.js-Applikation einzurichten, die auf http://testapp-maikw.rhcloud.com erreichbar ist. Zur Zeit sieht man dort noch nicht fiel, aber ich werde diesen Service unter anderem wahrscheinlich dazu nutzen, um meine Vertretungsplan-App mit einem CORS-Bypass auszustatten, der momentan über einen privaten Server läuft.

Wer also kostenlos mit ein paar Engines spielen möchte, dem sei OpenShift ans Herz gelegt. Wer über einen eigenen Webserver verfügt, der hat dadurch natürlich noch ganz andere Möglichkeiten.

Dennoch bietet OpenShift SSH-Zugang, Git Source Control und Unterstützung für viele Plattformen.

Netzwerkprotokoll programmieren #4

Heute habe ich das erste Mal die Bibliothek durch den Simulator von Atmel Studio 7 geschickt und mir sind einige Fehler in meiner Bibliothek aufgefallen. Hier eine grobe Übersicht:

  • index-Variable i (uint8_t) wurde runtergezählt und nach 0 kam 255
    • Die Kontrollabfrage durfte nicht i==255 lauten, sondern i < 255
  • sizeof() lieferte nicht die korrekte Länge des Arrays bei der Rückgabe der Adresse der ersten Speicherstelle (sizeof() gab 4 zurück)
    • strlen() aus der string.h löste das Problem (strlen() gab 101 zurück)
  • Die for-Schleife, die die Bits eines char’s des Textinhalts in das Paket-Array schreiben sollte, zählte einen zuviel und sprengte den Datenbereich des Pakets.

Bei der Analyse musste ich die I/O Ansicht und das Überwachungsfenster verwenden. Außerdem heftete ich mir die Laufvariablen im Editor an, um diese einfach überblicken zu können.


Nun zu der nächsten Funktion: Die BuildStringPackage()-Funktion.

char* BuildStringPackage(uint8_t dst, uint8_t src, char *data, size_t data_length)
{
	static char newPackage[PKG_C_SIZE + (PKG_S_SIZE * 8) + 4 + 1] = ""; // Standard-Größe von 17 plus Maximale Länge der Daten plus Länge des END_OF_TEXT Nibbles

	newPackage[0] = '1'; // START

	// Destination
	char dstArray[CMD_SIZE + 1] = "";
	SIConvertIntToBinaryString(dstArray, dst, sizeof(dstArray));
	newPackage[1] = *(dstArray + 0);
	newPackage[2] = *(dstArray + 1);
	newPackage[3] = *(dstArray + 2);
	newPackage[4] = *(dstArray + 3);

	// Source
	char srcArray[CMD_SIZE + 1] = "";
	SIConvertIntToBinaryString(srcArray, src, sizeof(srcArray));
	newPackage[5] = *(srcArray + 0);
	newPackage[6] = *(srcArray + 1);
	newPackage[7] = *(srcArray + 2);
	newPackage[8] = *(srcArray + 3);

	// Start of Text
	newPackage[9] =  '0';
	newPackage[10] = '0';
	newPackage[11] = '1';
	newPackage[12] = '0';

	if (data_length <= PKG_S_SIZE) // Ist die Länge der Daten kleiner oder gleich der erlaubten Maximallänge der Daten?
	{
		uint8_t i;
		for (i = 0; i <= data_length - 1; i++) // Durchgehen des Daten-Arrays | Stripping terminating zero
		{
			char characterArray[STR_SIZE + 1] = ""; // Länge eines Buchstabens + 1 (terminating zero)
			SIConvertIntToBinaryString(characterArray, *(data + i), sizeof(characterArray));

			for (uint8_t j = 0; j <= STR_SIZE; j++)
			{
				newPackage[12 + 1 + (i * 8) + j] = characterArray[j]; // Jedes Bit nacheinander in das Array schreiben
			}
		}

		while (i < PKG_S_SIZE) // Den Rest bis 10 Bytes voll sind, mit 0 auffüllen
		{
			char characterArray[STR_SIZE + 1] = "";
			SIConvertIntToBinaryString(characterArray, 127, sizeof(characterArray)); // 0x74 DEL

			for (uint8_t j = 0; j <= STR_SIZE; j++)
			{
				newPackage[12 + 1 + (i * 8) + j] = characterArray[j];
			}
			i++;
		}
	}

	// End of Text
	newPackage[12 + (PKG_S_SIZE * 8) + 1] = '0';
	newPackage[12 + (PKG_S_SIZE * 8) + 2] = '0';
	newPackage[12 + (PKG_S_SIZE * 8) + 3] = '1';
	newPackage[12 + (PKG_S_SIZE * 8) + 4] = '1';

	// End
	newPackage[12 + (PKG_S_SIZE * 8) + 4 + 1] = '0';
	newPackage[12 + (PKG_S_SIZE * 8) + 4 + 2] = '1';
	newPackage[12 + (PKG_S_SIZE * 8) + 4 + 3] = '0';
	newPackage[12 + (PKG_S_SIZE * 8) + 4 + 4] = '0';

	newPackage[12 + (PKG_S_SIZE * 8) + 4 + 4 + 1] = '\0'; // Terminating zero

	return newPackage;
}

Der erste Teil ist nahezu identisch mit der Funktion BuildPackage(), die mittlerweile BuildCommandPackage() heißt. Mittlerweile wurden auch einige Konstanten eingefügt, wodurch der Code etwas flexibler für die endgültige Definition des Protokolls wird.

  • PKG_C_SIZE = 17 | 17 Bits an Kommandos
  • PKG_S_SIZE = 10 | 10 Bytes an Daten (aktuell)
  • CMD_SIZE = 4 | Ein Kommando ist 4 Bit lang
  • STR_SIZE = 8 | Ein Buchstabe (char) ist 8 Bit lang

Mit ein wenig Mathematik kann die Funktion die entsprechenden Offsets zum Einfügen der Kommandos selber ermitteln. Ganz wichtig ist das beim Einfügen der Daten. Hier wird durch das Daten-Array durchiteriert und jeder Buchstabe wird in einen Binärstring umgewandelt und dann Bit für Bit von links nach rechts in den Paket-String eingearbeitet. Sollten Daten kleiner als die verfügbaren 10 Bytes sein, so wird der Rest mit einer 0 aufgefüllt (0x30 bzw. 48).

Ein Datenpaket kann wie folgt aussehen:

1|0001|0010|0011||0100|1000|0110|0001|0110|1100|0110|1100|0110|1111
|0111|1111|0111|1111|0111|1111|0111|1111|0111|1111||1001|0100

  • | markiert das Ende eines Nibbles
  • || markiert den Anfang und das Ende der Datenbytes

Wer Spaß an dem Entziffern von Binärzahlen hat, kann den Inhalt ja einmal entziffern. Die Daten sind im ASCII-Standard kodiert.
Die Lösung ist folgende:

0x1 - Start
0x1 - Zieladresse
0x2 - Absenderadresse
0x3 - Start der Daten
0x48 - H
0x61 - a
0x6C - l
0x6C - l
0x6F - o
0x7F - DEL-Steuerzeichen zum Auffüllen des Datenbereichs
0x9 - Ende der Daten
0x4 - Ende der Übertragung

Die Kommandos folgen nicht ganz exakt dem ASCII-Standard, soviel sei angemerkt.

Netzwerkprotokoll programmieren #3

Alle folgenden Artikel in dieser Artikelreihe finden Sie unter dem Tag Netzwerkprotokoll.

In diesem Artikel wird, wie versprochen, die Funktion ConvertIntToBinaryString besprochen.

void ConvertIntToBinaryString(char *resultArray, int number,
    size_t length)
{
	int count = 0;

	for (int i = length - 2; i >= 0; i--)
	{
		int bit = number >> i;

		if (bit & 1)
			*(resultArray + count) = '1';
		else
			*(resultArray + count) = '0';

		count++;
	}

	*(resultArray + length - 1) = '\0';
}

Diese Funktion erwartet die Adresse zur ersten Speicherstelle des Arrays, in das das Ergebnis geschrieben werden soll, die Zahl, die umgewandelt werden soll, und die Länge des Arrays. Die Notation wäre also im Pseudocode ConvertIntToBinaryString(dst, src, length). Da die Länge des Ergebnisarrays vom Programmierer vorgegeben ist, muss die Funktion die Größe des Arrays kennen.

In Zeile 4 wird eine Variable count auf 0 gesetzt. Diese wird in der folgenden for-Schleife hochgezählt. In der for-Schleife wird aber auch die Variable i heruntergezählt. Das hängt damit zusammen, dass der Mensch Binärzahlen von links nach rechts liest, also das MSB als erstes gelesen werden muss, und dies ist an der höchstwertigsten Stelle der Zahl.

Die Signatur der for-Schleife in Zeile 6 initialisiert die Variable i mit der Länge des Arrays – 2. Das hat damit zutun, dass die letzte Stelle in einem char-Array eine NULL ist, und von dieser Funktion nicht beachtet wird. Wiederum muss eine 1 dazu addiert werden, weil die Länge eines Arrays, also der Wert der Anzahl der Elemente eines Arrays immer einen Wert größer ist, als die adressierbaren Elemente eines Arrays (abzüglich der NULL).

Als erste Aktion innerhalb der for-Schleife in Zeile 8 wird die höchstwertigste Stelle der übergebenen Zahl per bitweisem Rechts-Shift auf die erste Stelle gesetzt und in die Variable bit gespeichert. Danach wird in Zeile 10 mit der bitweisen Operation der UND-Verknüpfung das gespeicherte Bit mit 1 verknüpft. Die 1 wird binär als 0000 0001 dargestellt.
Vereinfacht gesagt: Es wird überprüft, ob die Zahl an der ersten Stelle der Variable bit eine 1 ist. Wenn ja, dann wird eine 1 als char in das entsprechende Element im Ergebnisarray geschrieben, ansonsten eine 0.

Netzwerkprotokoll programmieren #2

Mittlerweile habe ich Atmel Studio 7 installiert. Fast sofort habe ich angefangen, den C#-Code auch in C zu implementieren. Hier musste ich auf einige Dinge achten:

  • Strings werden als char-Arrays gespeichert.
  • Die Konvertierung einer Ganzzahl in eine Binärdarstellung als String muss selber getätigt werden.
  • Der Zugriff auf Arrays über den lokalen Raum hinaus ist nicht ohne weiteres möglich.
    • Hier müssen Zeiger verwendet werden.

Für den heutigen Teil beschränke ich mich auf die Funktion, die mir aus den Parametern dst, src und packageType das Paket im String-Format baut.

char* BuildPackage(int dst, int src, int packageType)
{
	static char newPackage[17 + 1] = "";
	
	newPackage[0] = '1'; // START
	
	// Destination
	char dstArray[4 + 1] = "";
    ConvertIntToBinaryString(dstArray, dst, sizeof(dstArray));
	newPackage[1] = *(dstArray + 0);
	newPackage[2] = *(dstArray + 1);
	newPackage[3] = *(dstArray + 2);
	newPackage[4] = *(dstArray + 3);
	
	// Source
	char srcArray[4 + 1] = "";
    ConvertIntToBinaryString(srcArray, src, sizeof(srcArray));
	newPackage[5] = *(srcArray + 0);
	newPackage[6] = *(srcArray + 1);
	newPackage[7] = *(srcArray + 2);
	newPackage[8] = *(srcArray + 3);
	
	// Type
	char packageTypeArray[4 + 1] = "";
    ConvertIntToBinaryString(packageTypeArray, packageType, sizeof(packageTypeArray));
	newPackage[9] = *(packageTypeArray + 0);
	newPackage[10] = *(packageTypeArray + 1);
	newPackage[11] = *(packageTypeArray + 2);
	newPackage[12] = *(packageTypeArray + 3);
	
	// End
	newPackage[13] = '0';
	newPackage[14] = '1';
	newPackage[15] = '0';
	newPackage[16] = '0';

    newPackage[17] = '\0';
	
	return newPackage;
}

Die interessanten Codezeilen sind markiert. Die Funktionssignatur beginnt mit char*: Der Rückgabewert ist vom Typ Pointer auf ein Char-Datum. Mit diesem Mechanismus können statische Arrays zurückgegeben werden. Die Funktion ConvertIntToBinaryString() wird im nächsten Teil erklärt. Es ist wichtig, dass das char-Array als static deklariert wird. Außerdem muss dem char-Array am Ende eine sog. terminating zero hinzugefügt werden. Anhand dieser erkennen Funktionen, wann ein char-Array endet.

Beim Deklarieren und Definieren von Arrays wird ein freier Speicherblock gesucht und die erste freie Speicherstelle wird in die Variable des Arrays gespeichert (s. Code-Beispiel). Die restlichen Speicherstellen des Arrays werden dann reserviert. Ein korrekt definiertes char-Array besitzt immer eine Speicherstelle mehr als der tatsächliche Textinhalt.

char someText[3] = "Hi";

// Gibt die Adresse der 1. Speicherstelle des Arrays aus
printf("%p", someText); 

// Gibt den Wert an der 1., 2. und 3. Speicherstelle des Arrays aus
printf("\n%c%c %d", someText[0], *(someText + 1), *(someText + 2));

// Gibt den String komplett aus
printf("%s", someText);

Array zu klein

Die Länge des Arrays muss der Funktion übergeben werden, damit die Adresse hochgezählt werden kann, und so auf die Daten zugegriffen werden kann.
Erhält man einen Pointer, so enthält dieser die Adresse zum Inhalt, *pointer ist dann der Wert an dieser Adresse. Folglich kann mit *(pointer + i) auf die verschiedenen Daten im Array zugegriffen werden. Dies passiert auch in der Funktion BuildPackage().

Die Funktion wird dann wie folgt aufgerufen:

char *resultCharArray;
resultCharArray = BuildPackage(0x1, 0x2, 0x3);

Weiteres Infomaterial [en]: Tutorialspoint