Verteiltes Malen - sandro
Transcription
Verteiltes Malen - sandro
Projekt zu den Vorlesungen des WS 03/04 „Eigenschaften mobiler und eingebetteter Systeme“ und „Zuverlässige Systeme“ von Christoph Graupner und Sandro Schwarz Verteiltes Malen 1. Einführung 2. Bluetooth und IrDA 3. Das Programm 3.1. Implementierung des Malens 3.2. Datenstruktur der Übertragungspakete und Übertragungsprotokoll 3.3. Kommunikation 3.3.1. Senden 3.3.2. Empfangen 3.3.3. Update der Oberfläche 4. Tradeoffs 4.1. Performance 4.2. Probleme 1. Einführung Das Ziel dieses Projektes war es, ein verteiltes Malprogramm zu entwickeln, welches mit Hilfe von zwei PDAs (2 Dell Axim X5) es dem Anwender erlaubt, mit einer anderen Person im selben Bild zu malen. Als Plattform hierfür dient Windows CE . NET 4.2. Die Software der Malapplikation wurde in C# und C++ erstellt. Hierbei diente als Entwicklungsumgebung Microsoft Visual Studio .NET 2003 und Microsoft eMbedded Visual C++ 4. Wir haben mit Hilfe von C# die Oberfläche zum Malen erstellt und steuern hierüber auch eine in nativ-C++ erstellte DLL an, welche die Kommunikation mit den beiden PDAs übernimmt. Diese Vorgehensweise war nötig, da wir uns beim Beschäftigen mit Bluetooth damit konfrontiert sahen, dass C# noch keine Schnittstellen zu Bluetooth-Funktionalitäten anbietet. In diesem Zusammenhang sei vorweg aber auch schon einmal erwähnt, dass wir uns dann zu einem noch späteren Zeitpunkt auf Grund von für uns unüberbrückbaren Schwierigkeiten mit Bluetooth und den installierten Anycom-Bluetooth-Karten dazu entschieden haben, als Übertragungsarchitektur IrDA zu verwenden. Jedoch haben wir uns weiterhin an diese zweigeteilte Struktur gehalten, um auf diese Weise immer noch so dicht wie möglich an der eigentlichen Aufgabenstellung, welche das Arbeiten mit Bluetooth vorsieht, zu bleiben. Beim Malen spielt es keine Rolle, ob beide Anwender in derselben oder in unterschiedlichen Farben malen. Durch ein zuverlässiges Übertragungsprotokoll wird sichergestellt, dass auf beiden Displays das Gleiche zu sehen ist. Es können zwar kleinere Zeitverzögerungen auftreten, so dass ein kleines Delay von den beiden Partnern zu beobachten ist, aber in den meisten Fällen sollte die Übertragung sehr schnell erfolgen. Da unser Programm von der Zusammenarbeit zweier Anwender lebt, ist die Schnelligkeit der Übertragung auch eine der wichtigsten Eigenschaften des Malprogrammes, deshalb haben wir z.B. auch auf eine möglichst kleine Paketgröße für den Informationstransfer geachtet. Nach dem Start der Software auf den zwei Geräten muss zuerst über ein Menü die Verbindung mit dem jeweils anderen Gerät angefordert werden, die Kommunikation zwischen den beiden PDAs läuft danach komplett selbstständig im Hintergrund ab, und benötigt erst wieder ein Eingreifen durch den Benutzer, wenn die Verbindung vollständig abgerissen sein sollte. Auf der Fehlertoleranz liegt bei der kabellosen Datenübertragung natürlich ein besonderes Augenmerk, deshalb fängt unser Protokoll verschiedene zu erwartende Fehler, die bei der Übertragung auftreten können, in der Übertragungsschicht ab, egal ob nun die Übertragung mit Bluetooth oder IrDA stattfindet. So ist gewährleistet, dass die eigentliche Anwendung nicht erneut entwickelt werden muss, wenn Bluetooth eines Tages funktionieren sollte. 2. Bluetooth und IrDA Zu Beginn unseres Projektes untersuchten wir zuerst, welche Plattformen wir zum Entwickeln auf den PDA nutzen konnten. Es war das .NET Compact Framework (.NET CF) und native-eMbedded C++ (Microsoft Foundation Class (MFC) Programmierung). Das .NET Compact Framework und auch das umfangreichere .NET Framework unterstützen kein Bluetooth, die MFC-Klassen jedoch schon. IrDA wird von beiden Plattformen von Haus aus unterstützt. Da sich in das .NET CF native DLLs per Plattform Invoke einbinden lassen, kann man auch, bei Entwicklung einer entsprechenden Bluetooth-DLL, auch vom .NET CF aus Bluetooth ansprechen. Es gibt auch noch einen anderen Weg um mit .NET Bluetooth ohne nativeBluetooth-DLL zu sprechen: über das COM-Profil des Bluetooth Standards. Leider wird dieses Profil von den Anycom Karten nicht unterstützt. Im Internet war es nicht leicht Beispiele für die Programmierung von Bluetooth Applikationen mit Windows CE zu finden. Das Programm, das das COM-Profil nutzte funktionierte nicht, die Beispielprogramme von Microsoft, die man mit dem PlattformBuilder 4 erhält, ließen sich nicht übersetzen und waren nicht kommentiert. Nach der Adaption an unsere Entwicklungsumgebung (eMbedded Visual C++) der Microsoftbeispiele liefen sie zwar, aber es kam keine Kommunikation, nicht einmal ein Auffinden der möglichen Kommunikationspartner, zwischen den PDAs zustande. Die Beispiele nutzten die Windows Sockets. Da IrDA ebenfalls mit Windows Sockets anzusprechen war und die Zeit drängt, gingen wir zu IrDA als Kommunikationsarchitektur über. Die Tatsache, dass IrDA und Bluetooth über die Windows Sockets nutzen konnte, ließ uns bei dem ursprünglichen Design (eine Kommunikations-DLL in eMbedded C++ und das UI auf .NET Basis) bleiben, da man später einfach die IrDA-DLL durch eine Bluetooth-DLL austauschen könnte. Daher gelten prinzipiell folgende Beschreibungen auch für die Bluetooth Implementation. IrDA ist jedoch von der Übertragungskapazität her langsamer als Bluetooth, ist aber für unsere Zwecke ausreichend, da wir nur wenige Daten (6 Byte/Paket) verschicken. Ein weiterer Nachteil zu Bluetooth ist, das sich die Partner im Sichtkontakt befinden müssen und demzufolge auch einfach auf dem anderen PDA malen und sehen könnten. Bluetooth reicht aber auch durch nicht zu dicke Wände und ist für mehr als nur zwei Kommunikationspartner geeignet, was wir in unserem Protokoll berücksichtigen. 3. Das Programm Da kein überschaubarer Sourcecode für ein einfaches Malprogramm im Internet zu finden war, haben wir ein eigenes kleines Programm nach unseren Anforderungen schreiben müssen. Dabei haben wir nur die wichtigsten Funktionalitäten eingefügt. Deshalb kann man mit unserer Software nur Freihandzeichnen, und es wurde von uns nur eine kleine Auswahl an Farben implementiert, so schaffen wir es auch die zu übertragenden Datenpakete so klein wie nur möglich zu halten, so dass auch die Übertragungszeit so kurz wie nur möglich gehalten wird. Eine weitere Funktionalität ist das Löschen des Bildes. Sobald dieser Button von einem der beiden User betätigt wurde ist diese Nachricht dominant und löscht die Bilder auf beiden Displays. Hier müssen wir natürlich von einer gewissen Fairness der beiden Anwender ausgehen, da sich aber beide bei einer Übertragung mittels IrDA im selben Raum befinden müssen, setzen wir dies einfach voraus. 3.1. Implementierung des Malens Um unser Hauptaugenmerk dem Protokoll widmen zu können, haben wir die Malapplikation in C# und mit dem .NET Compact Framework entwickelt. Es ist wesentlich leichter und schneller zu implementieren als mit den MFC. Die Oberfläche lässt sich mit den Tools des Visual Studios .NET zusammenklicken und das .NET Framework ruft Ereignisbehandlungsroutinen auf, die man z.B. Dem Ereignis „Stiftauf-Screen-gedrückt“ zugeordnet hat. Für das Malen auf dem Screen sind 3 Ereignisse wichtig: OnMouseDown, OnMouseMove, OnMouseUp. OnMouseDown stellt den Beginn eine neuen zu zeichnenden Linie dar und OnMouseUp das Ende. Bei einem der beiden Ereignisse zeichnet man einen Punkt auf der Malfläche, damit auch ein einfacher Punkt gemalt werden kann, weil bei einem Punkt keine Stiftbewegung (MouseMove) stattfindet. OnMouseMove leistet die Hauptarbeit beim Malen. Es malt eine Linie in der vorher eingestellten Farbe von dem letzten bekannten Punkt (gemerkt beim letzten Aufruf der OnMouseMove-Behandlungsroutine) zum gerade eben bekannten neuen Punkt. Die neue Position des Stifts wird für den nächsten Aufruf gemerkt. Bei der Behandlung der Ereignisse OnMouseDown und OnMouseMove, werden auch die Farbe und die Position des Stifts an den Partner übermittelt. 3.2. Datenstruktur der Übertragungspakete Daten im Packet Kopf(8bit) Packet(8bit) x(16bit) y(16bit) Daten im Kopf-Feld CheckFlag(1) SenderID(3) Color(3) struct HEAD { unsigned char CheckFlag : 1; unsigned char SenderID : 3; unsigned char Color : 3; unsigned char BeginOfDraw }; union PACKETNUMS { unsigned char sending; unsigned char confirm; }; struct DATA{ HEAD Kopf; PACKETNUMS packet; short x; short y; }; union PACKET{ DATA Daten; char CharSend[sizeof(DATA)]; }; BeginOfDraw(1) // ?0000000 // 0???0000 // 0000???0 : 1; // 0000000? Die Daten, welche für die Übermittlung der Informationen zwischen den PDAs hin und hergeschickt werden, sind wie folgt strukturiert. Wir haben mit den von C++ zur Verfügung gestellten Datenkonstrukten „struct“ und „union“ unsere Datenpakete in Kopf, Paketnummerierung und jeweils ein Datum für die x- und y-Koordinate unterteilt. Im Kopf stehen gleich mehrere Informationen in insgesamt 8 bit kodiert. Das erste Bit des Kopfes (CheckFlag) bestimmt, ob das Paket ein Steuer- (= 1) oder ein Datenpaket (= 0) ist. Die spezielle Bedeutung des Steuerpaketes im einzelnen Fall wird dann in den 3 Bit des „Color“-Feldes, welche ansonsten einen Farbcode enthalten, codiert. Für den Fall, dass das CheckFlag Bit auf 1 gesetzt ist, bedeutet eine Belegung von „000“ bei Color, dass auf dem anderen PDA die Taste „Bild löschen“ gedrückt wurde, und nur diese Information wird an die Applikation weitergeleitet, die beiden anderen, momentan gültigen Codes im Color-Feld werden bereits auf dem Kommunikationslayer bearbeitet. Sollte in Color „001“ stehen, so wird vom anderen Gerät eine Bestätigung darüber angefordert, dass die letzten 125 Pakete richtig angekommen sind. Falls dies stimmt, wird ein Paket mit gesetztem CheckFlag, einer „010“ in Color und der Paketnummer des 124ten Pakets in „packet“ versandt. Sollte jedoch ein Fehler aufgetreten sein, so steht an letzt genannter Stelle die Nummer des Paketes, welches als letztes richtig angekommen ist. In einer solchen Situation reagiert das System mit der erneuten Sendung der Pakete, welche diesem letzten korrekten Paket folgen. Wenn das CheckFlag auf null gesetzt ist, so enthält das Color-Feld natürlich die Farbe der aktuellen Aktion, BeginOfDraw zeigt mit einer 1 an, dass der Stift gedrückt wurde und damit eine neue Linie begonnen wurde. Alle folgenden Pakete dieser Strichführung haben an dieser Stelle dann eine 0. Um dieses Protokoll auch für eine Struktur mit mehr als 2 Teilnehmern (z.B. Bluetooth) nutzen zu können, haben wir noch 3 bits für die SenderID eingefügt. Mit Hilfe des Feldes „packet“ können wir einerseits die Paketnummer des jeweiligen Paketes senden, oder wie bereits erwähnt im Falle einer Confirm-Nachricht die Paketnummer des zuletzt richtig empfangenen Paketes. Als letztes sind noch die Koordinaten in unserem Paket unterzubringen. Dies geschieht mit den zwei short-werten x und y, so dass jeweils 16 Bit zur Verfügung stehen. Da das Display unserer zur Verfügung gestellten Dell Axim X5 weit kleiner als 2 hoch 16 ist, aber Werte größer als 255 liefern kann sind 16 Bit angebracht und ausreichend. 16Bit auch deshalb um eine Umwandlung von 9 in 16 bit Werte zu vermeiden und ein Byte-Alignment zu erreichen. Mit Hilfe der union PACKET ist es uns möglich auf die Paketstruktur zuzugreifen, als wäre es ein Bytearray. Dadurch ist es möglich ein Paket, so wie es ist per Windows Sockets zu übertragen. 3.3. Kommunikation Betrifft: drawing/xchg.cs,drawing/data.cs, comm_paint/* Die Kommunikation über IrDA erfolgt im Zweikanalbetrieb, d.h. Ein Kanal zum Senden (Client) und einer zum Empfangen (Server). Ein Server der per Winsocks socket(),bind(),listen(),accept() an einem Port auf einen Client warte und nach Verbindungsaufbau mit diesem dort die Daten empfängt. Und ein Client der per Winsocks socket(),connect() und unser Routinen zum Finden von IrDA-Geräten (oder Bluetooth, wenn's denn mal unterstützt wird) (GetDeviceBegin(), GetDeviceFirst(), GetDeviceNext(), GetDeviceEnd()) einen laufenden Malprogramm-Server auf anderen PDA kontaktiert und an ihn nach Verbindungsaufbau die Daten sendet. Client und Server existieren jeweils Paarweise auf einem PDA und jeder in seinem eigenen Thread, so dass sie asynchron senden/empfangen können und parallel zur Benutzeroberfläche arbeiten können. Im Teil, der fürs Empfangen zuständig ist, läuft noch ein weiterer Thread, der Daten aus dem Empfangspuffer zur Oberfläche weiterreicht. Somit arbeiten 4 Threads auf einem PDA: die Oberfläche, die Senderoutine, die Empfangsroutine und die Empfangs-zu-oberfläche-transferroutine. Bei allen Routinen gilt: Treffen sie nicht auf das erwartete, so beenden sie sich durch den Wurf einer Exception und der Terminierung des Programms. Den Sende- und Empfangsroutinen ist gemein, dass sie so lange warten bis sowohl der Client als auch der Server einen Partner haben, ehe sie mit ihrer Arbeit beginnen. 3.3.1. Senden Betrifft: drawing/xchg.cs: xchg.send(), xchg.send(,,) comm_paint/comm_sendthrough.cpp: send(...) Das Senden passiert aus Gründen der Zuverlässigkeit und der möglichen unterschiedlichen Geschwindigkeiten des Programms und des eigentlichen Sendevorgangs mit Hilfe von einem Sende- und einem Empfangspuffer mit einer Größe von je 250 Pakten. Die hier besprochene Senderoutine benutzt den Sendepuffer. Sobald der Anwender seinen Stift auf das Display setzt werden Nachrichtenpakete initiiert. Diese werden dann innerhalb des C#-Programmes asynchron zu Versendung per IrDA in den Puffer geschrieben und nacheinander ausgelesen und mit der send()-Funktion, die die Windows Sockets send()-routine kapselt, an den anderen PDA versandt. Um den Puffer gut kontrollieren zu können, haben wir 3 Pointer eingefügt: firstfree, nextsend und notconfirm. Firstfree zeigt auf die Stelle im Puffer, in welche die von der Applikation ankommenden Daten geschrieben werden können und wird nach jedem neuen Eintrag um eins erhöht. Nextsend zeigt auf die Stelle des Sendepuffers, in der das nächste zu sendende Datenpaket wartet. Notconfirm weist auf die Position im Puffer, wo sich das Paket befindet, welches zwar schon abgesandt wurde, aber vom anderen PDA noch nicht bestätigt wurde, dass es korrekt angekommen ist. Alle diese Pointer stehen in Beziehung zu einander. Und an Hand dieser Beziehungen können Spezialfälle abgefragt werden. Wenn firstfree und nextsend auf dieselbe Stelle im Sendepuffer verweisen bedeutet dies, dass der Puffer leer ist und der Kommunikationslayer entweder alle nachrichten bereits abgeschickt hat, oder dass es noch keine gab. Bei einem vollen Puffer zeigen die beiden Pointer firstfree und notconfirm im Gegensatz dazu auf ein und dieselbe Position im Sendepuffer. Wenn ein Confirm angefordert wurde, so hat dies Vorrang, und das zu sendende Datenpaket wird vor allen anderen versandt. Diese zu versende Paket wird von der Empfangsroutine in einen speziellen Puffer geschrieben, der genau ein Paket aufnehmen kann. Etwas anderes ist es, wenn das Programm selbst ein Confirm anfordert. Es wird in der Senderoutine erstellt und ohne zwischenspeichern verschickt. Dies geschieht immer dann, wenn der Pointer nextsend um eine Stelle größer ist als 124 bzw. 249. Somit fordern wir also immer nach 125 Paketen eine Bestätigung an. Und auf diesem Wege lasten wir unseren Puffer auch besser aus und schonen vor die IrDAKommunikation vor übermäßigen Datenverkehr, da auf die zweite hälfte des Puffers weiterhin geschrieben werden kann. Sollte als Antwort auf das Confirm-Paket eine Nachricht folgen, die eine Paketnummer kleiner ungleich 124 oder 249 bestätigt, so wird nextsend und notconfirm einfach auf die Stelle im Puffer ausgerichtet, wo das Paket liegt, welches der andere PDA als erstes noch nicht erhalten hat. Dies geschieht von der Empfangsroutine aus, da diese die benötigte Adresse geschickt bekommen hat. Von hier an geht alles so weiter wie zuvor. 3.3.2. Empfangen Betrifft: drawing/xchg.cs: read(), readfrombuf() comm_paint/comm_readhrough.cpp: commReadThrough() Das Empfangen ist zu jeder Zeit möglich, außer der Empfangspuffer ist voll, da der Empfang in einem eigenen Thread läuft. Die Daten werden durch die DLL-Routine commReadThrough() von der IrDASchnittstelle gelesen, auf Empfangsfehler (z.B. falsche Größe), aber nicht auf korrekten Inhalt geprüft und zur C#-Routine read() durchgereicht und Fehler signalisiert (Returncode). In dieser C#-Routine wird erst geprüft, ob noch Platz im Empfangspuffer ist und dann ob ein Steuer- oder Datenpaket eingetroffen ist und dementsprechend gehandelt. Sollte kein Platz im Puffer sein, wird eine Exception geworfen und das Programm beendet sich. Ist ein Steuerpaket eingetroffen, wird geschaut ob ein ClearScreen-Paket, ein Bestätigungsanforderungspaket oder eine Antwort auf ein Bestätigungsanforderungspaket eingetroffen ist. Bei ersterem wird das Paket in den Empfangspuffer gestellt, um die Reihenfolge der Aktionen zu gewährleisten und zusätzliche Interaktion zwischen Empfangsroutine und Oberfläche zu vermeiden. Bei einem Bestätigungsanforderungspaket wird ein Antwortpaket darauf erstellt und in einem speziellen Puffer gespeichert, den die Senderoutine bevorzugt sendet, und ein Signal für die Senderoutine gesetzt, dass ein wichtiges Paket im Spezialpuffer liegt. Trifft die Antwort auf ein Bestätigungsanforderungspaket ein wird der notconfirmPointer des Sendepuffers auf die empfangene bestätigte Paketnummer gesetzt. Ist diese nicht 124 oder 249 wird auch der nextsend-Pointer angepasst, damit er die verlorenen Pakete noch mal sendet. Trifft kein Steuerpaket sondern ein Datenpaket ein, so wird es in den Empfangspuffer nach der Prüfung der Paketnummer gestellt und dessen Zeiger angepasst. Danach beginnt alles wieder von vorn. Der „torecv“-Pointer des Empfangspuffers zeigt auf den Platz im Puffer, der das nächste Paket aufnehmen soll und gibt gleichzeitig die erwartete Paketnummer an, auf die ein Paket geprüft wird. Gelingt der Vergleich nicht, wird das Paket verworfen. Der „lastfetched“-Zeiger des Empfangspuffers, gibt das von der readfrombuffer ()-routine zuletzt an die Oberfläche weitergeleitete Paket an. Die readfrombuffer()-routine läuft in einem anderen Thread als die read()routine, damit auch Daten an die Oberfläche weitergeleitet werden können, wenn keine Pakete empfangen werden und Pakete empfangen werden können, wenn die Oberfläche beschäftigt ist. Der Transfer der Pakete vom Puffer zur Oberfläche geschieht mit Hilfe einer Callback-Funktion, die dem Konstruktor der xchg-Klasse übergeben wurde. Der Callbackfunktion wird, wenn der Puffer Daten hat (lastfetch+1 != torecv), das Paket als Funktionsparameter mitgegeben. 3.3.3. Update der Oberfläche Die Oberfläche muss an Stellen ihr Bild auffrischen, wenn gemalt wird. Einmal wenn der Nutzer des lokalen PDAs malt und wenn Daten vom Remote-PDA empfangen wurden. Ersteres wurde unter 3.1. schon beschrieben und letzteres folgt nun. In der Callback-Funktion, die dem Konstruktor der xchg-Klasse mitgegeben wurde, wird zuerst geprüft, ob ein Steuerpaket oder ein Datenpaket eingetroffen ist. Beim Steuerpaket kann es sich nur um einen ClearScreen-Befehl handeln und der Bildschirm wird gelöscht. Bei einem Datenpaket wird geprüft, ob dass der Beginn einer neuen Linie ist oder nicht. Wenn ja wird der gemerkte Punkt vom letzen Aufruf der Callbackfunktion vergessen und dann nur der neue Punkt gemalt. Wenn nein, wird mit Hilfe des gemerkten Punktes eine Linie zwischen altem und neuem Punkt gemalt. Das Malen des lokalen Dateninputs geschieht völlig unabhängig zu dem des Malens der Remotedaten, da sie in verschieden Threads laufen. 4. Tradeoffs 4.1.Performance Durch die Verwendung des .NET Framework und dessen Laufzeitumgebung ist die Darstellung langsamer als wenn die native MFC Implementierung dazu gewählt worden wäre. Die Performance lässt selbst bei einem singlethreaded Malen ohne Datenverschickung zu wünschen übrig. Mit Threading und Datenaustausch ergeben sich erkennbare Verzögerungen, die sich jedoch erst bei gleichzeitigem Malen beider Teilnehmer wirklich störend werden. Zeitmessung waren nicht möglich, da die Zeitnahme im .NET CF anscheinend zu ungenau ist, da sie nur beim der ersten Messung eines Aufruf der zur untersuchenden Funktion etwas ungleich null lieferte. An Echtzeit ist in dieser Konfiguration nicht zu denken, selbst wenn Bluetooth mit den höheren Datenraten genutzt wird, da die Oberfläche zu träge ist. Da hilft nur noch ein Prozessorupdate oder MFC Programmierung, was aufwendig ist. 4.2.Probleme Probleme traten vor allem bei der Nutzung des .NET Compact Framework auf, da sich dieses vom .NET Framework teilweise stark unterscheidet. Es fehlen Bibliotheksfunktionen (z.B. Thread.abort(), Controls.Handle), die in der Hilfe erwähnt sind, aber im CF dann doch fehlen. Das Marshalling zwischen native-Code und .NET managed-Code ist abweichend vom „großen“ Framework und bereitet Probleme, bei der Übergabe von Callback-Funktionszeigern. Die Microsoft Hilfen zu Visual Studio .NET 2003 (MSDN-Hilfe) sind auch schlecht gepflegt in Bezug zum Compact Framework. Bei den Bluetooth-Beispielen fehlte z.B. die Angabe wo die Sourcen zu finden sind. Generell ist wenig zu Bluetooth an Programmierbeispielen zu finden und wenn man die wenigen Raritäten findet, funktionieren sie nicht, weil die Anycom Karten irgendwie zu nichts kompatibel sind. Nicht mal das COM-Profil wird unterstützt. Mit IrDA gab es keine Probleme.