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.