Difference between revisions of "Tutorial:Serielle Kommunikation"
(german translation) |
(stuff for serial device in german) |
||
Line 5: | Line 5: | ||
== Vorbereiten des seriellen Gerätes == | == Vorbereiten des seriellen Gerätes == | ||
− | Als | + | Als Modelgerät benutzen wir ein Arduino UNO welches per RS232 am Amiga angeschlossen wird bzw. via USB am MorphOS Computer und dabei einen virtuellen seriellen Port zur Verfügung stellt. Achtung man kann nicht einfach die Leitungen des Arduino an den Amiga RS232 anschliessen, da braucht man ein wenig Elektronik um die Pegel zu regeln. Bei Interesse nach "RS232 Shield Arduino" oder "RS232 zu TTL-Konverter" googlen, da gibt es viel Auswahl. |
Zuerst muss der Arduino programiert werden. Als sehr einfacher Anfang wir schreiben ein kurzes Program welches nur "Hello World" und eine laufende Nummer ausgibt. (damit man besser sieht das etwas passiert.) | Zuerst muss der Arduino programiert werden. Als sehr einfacher Anfang wir schreiben ein kurzes Program welches nur "Hello World" und eine laufende Nummer ausgibt. (damit man besser sieht das etwas passiert.) | ||
Line 183: | Line 183: | ||
Res: LongInt; | Res: LongInt; | ||
DevOpen: Boolean = False; | DevOpen: Boolean = False; | ||
− | DeviceName: AnsiString = ' | + | DeviceName: AnsiString = 'serial.device'; // or usbmodem.device |
UnitNumber: Integer = 0; | UnitNumber: Integer = 0; | ||
Line 205: | Line 205: | ||
Zuerst wir konfigurieren den IORequest: | Zuerst wir konfigurieren den IORequest: | ||
<source lang="pascal"> | <source lang="pascal"> | ||
− | io^.io_SerFlags := (io^.io_SerFlags or SERF_XDISABLED) and (not SERF_PARTY_ON); | + | // Parity und XON/XOFF ausschalten |
− | io^.io_Baud := Baud; | + | io^.io_SerFlags := (io^.io_SerFlags or SERF_XDISABLED) and (not SERF_PARTY_ON); |
− | io^.IOSer.io_Command := SDCMD_SETPARAMS; | + | io^.io_Baud := Baud; // Baud rate setzen |
+ | io^.io_ReadLen := 8; // 8 Bits pro Character | ||
+ | io^.io_WriteLen := 8; | ||
+ | io^.io_StopBits := 1; // 1 Stopbit | ||
+ | io^.IOSer.io_Command := SDCMD_SETPARAMS; // Message Kommando = Setze Parameter | ||
</source> | </source> | ||
Um die Nachricht zu versenden wir können <code>DoIO()</code> oder <code>SendIO()</code> neutzen. DoIO() blockiert bis die Nachricht abgearbeitet ist. SendIO() kehrt sofort zurück, läuft im Hintergrund weiter und wir müssen testen ob die Nachricht beendet ist. I prefer SendIO() because you have more control about what happens and you can break the request if it needs too long time. With <code>CheckIO()</code> you can test if the request is already finished or use WaitIO() for the end of operation. All together it looks like that: | Um die Nachricht zu versenden wir können <code>DoIO()</code> oder <code>SendIO()</code> neutzen. DoIO() blockiert bis die Nachricht abgearbeitet ist. SendIO() kehrt sofort zurück, läuft im Hintergrund weiter und wir müssen testen ob die Nachricht beendet ist. I prefer SendIO() because you have more control about what happens and you can break the request if it needs too long time. With <code>CheckIO()</code> you can test if the request is already finished or use WaitIO() for the end of operation. All together it looks like that: | ||
<source lang="pascal"> | <source lang="pascal"> | ||
− | io^.io_SerFlags := (io^.io_SerFlags or SERF_XDISABLED) and (not SERF_PARTY_ON); | + | // Parity und XON/XOFF ausschalten |
− | io^.io_Baud := Baud; | + | io^.io_SerFlags := (io^.io_SerFlags or SERF_XDISABLED) and (not SERF_PARTY_ON); |
− | io^.IOSer.io_Command := SDCMD_SETPARAMS; | + | io^.io_Baud := Baud; // Baud rate setzen |
+ | io^.io_ReadLen := 8; // 8 Bits pro Character | ||
+ | io^.io_WriteLen := 8; | ||
+ | io^.io_StopBits := 1; // 1 Stopbit | ||
+ | io^.IOSer.io_Command := SDCMD_SETPARAMS; // Message Kommando = Setze Parameter | ||
SendIO(PIORequest(io)); // sende die Nachricht | SendIO(PIORequest(io)); // sende die Nachricht | ||
WaitIO(PIORequest(io)); // Warte auf die Antwort | WaitIO(PIORequest(io)); // Warte auf die Antwort | ||
Line 241: | Line 249: | ||
exec, serial; | exec, serial; | ||
const | const | ||
− | DefDevice = ' | + | DefDevice = 'serial.device'; // or usbmodem.device |
DefUnit = 0; | DefUnit = 0; | ||
DefBaud = 9600; | DefBaud = 9600; | ||
Line 280: | Line 288: | ||
io^.io_SerFlags := (io^.io_SerFlags or SERF_XDISABLED) and (not SERF_PARTY_ON); | io^.io_SerFlags := (io^.io_SerFlags or SERF_XDISABLED) and (not SERF_PARTY_ON); | ||
io^.io_Baud := Baud; | io^.io_Baud := Baud; | ||
+ | io^.io_ReadLen := 8; // 8 Bits pro Character | ||
+ | io^.io_WriteLen := 8; | ||
+ | io^.io_StopBits := 1; // 1 Stopbit | ||
io^.IOSer.io_Command := SDCMD_SETPARAMS; | io^.IOSer.io_Command := SDCMD_SETPARAMS; | ||
SendIO(PIORequest(io)); | SendIO(PIORequest(io)); | ||
Line 324: | Line 335: | ||
<source lang="pascal"> | <source lang="pascal"> | ||
− | io^.io_SerFlags := (io^.io_SerFlags or SERF_EOFMODE or SERF_XDISABLED) and (not SERF_PARTY_ON); | + | // Parity und XON/XOFF ausschalten, EOF anschalten |
− | io^.io_Baud := Baud; | + | io^.io_SerFlags := (io^.io_SerFlags or SERF_EOFMODE or SERF_XDISABLED) and (not SERF_PARTY_ON); |
− | io^.io_TermArray.TermArray0 := $0a030303; | + | io^.io_Baud := Baud; // Baudrate setzen |
+ | io^.io_ReadLen := 8; // 8 Bits pro Character | ||
+ | io^.io_WriteLen := 8; | ||
+ | io^.io_StopBits := 1; // 1 Stopbit | ||
+ | io^.io_TermArray.TermArray0 := $0a030303; // Ende des Files Zeichen #10 Rest gefüllt mit $03 = End of Text | ||
io^.io_TermArray.TermArray1 := $03030303; | io^.io_TermArray.TermArray1 := $03030303; | ||
− | io^.IOSer.io_Command := SDCMD_SETPARAMS; | + | io^.IOSer.io_Command := SDCMD_SETPARAMS; // Nachtichten Kommando type = Setze Parameter |
SendIO(PIORequest(io)); // Sende Nachricht | SendIO(PIORequest(io)); // Sende Nachricht | ||
WaitIO(PIORequest(io)); // Warte auf Antwort | WaitIO(PIORequest(io)); // Warte auf Antwort |
Revision as of 18:18, 20 December 2018
Start Nächste
Dieses Tutorial zeigt wie man Geräte mit serieller Kommunikation (RS232 oder USB) am Amiga benutzt. Als Besipiel wird hier ein Arduino UNO board benutzt. Alles was hier gezeigt wird wurde auf einem Amiga 1200 (68030/50 Mhz, OS 3.9) und einer MorphOS machine getestet, aber sollte auch genauso auf einem AmigaOS4 oder nativen AROS (i386/x86_64/ARM) so funktionieren.
Vorbereiten des seriellen Gerätes
Als Modelgerät benutzen wir ein Arduino UNO welches per RS232 am Amiga angeschlossen wird bzw. via USB am MorphOS Computer und dabei einen virtuellen seriellen Port zur Verfügung stellt. Achtung man kann nicht einfach die Leitungen des Arduino an den Amiga RS232 anschliessen, da braucht man ein wenig Elektronik um die Pegel zu regeln. Bei Interesse nach "RS232 Shield Arduino" oder "RS232 zu TTL-Konverter" googlen, da gibt es viel Auswahl.
Zuerst muss der Arduino programiert werden. Als sehr einfacher Anfang wir schreiben ein kurzes Program welches nur "Hello World" und eine laufende Nummer ausgibt. (damit man besser sieht das etwas passiert.)
const unsigned long BAUD_RATE = 9600;
int count;
void setup() {
Serial.begin(BAUD_RATE);
count = 1;
}
void loop() {
Serial.print("Hello Amiga (Msg: ");
Serial.print(count++);
Serial.println(")");
delay(500);
}
Dieses Programm sendet jede halbe Sekunde ein "Hello World (Msg: count)" and den serielle port. Dies kann man ganz gut mit dem "Serial Monitor" der Arduino IDE überprüfen
Serielle Ausgabe am Amiga
Nun wollen wir diesen Text auch am Amiga sehen, wenn der Arduino am RS232 angeschlossen ist muss man nur ein Terminalprogramm starten wie z.B. NComm[1]. Dort setzt man das Device zu serial.device, die Unitnummer zu 0 und Baudrate zu 9600 und schon sollte man den Output des obigen Programms sehen.
Wenn man den Arduino per USB anschließt sollte der installierte USB Stack (z.B. Poseidon) automatisch den richtigen Treiber (cdcacm.class) benutzen und damit den Virtuellen seriellen Port verfügbar machen. Das sollte ungefähr so aus sehen:
Falls er nicht automatisch den richtigen Treiber benutzt (z.B. auf MorphOS) muss man diesen erzwingen für den 0. Endpunkt. (USB Prefs/Trident öffnen, Devices, Doppelklick auf"Arduino UNO", "CDC control interface (0)" auswählen, rechte Maustaste "forced binding" "cdcacm.class", Warnung abnicken, Usb rausziehen und wieder anschliessen). Dieser virtuelle serielle port soltle jetzt verfügbar sein über das usbmodem.device. Der Rest ist gleich wie beim RS232 Anschluss nur das das device usbmodem.device heisst.
Serielles Gerät mit FreePascal
Message port
Als erstes brauchen wir einen Messageport für die Kommunikation mit dem Triber, dafür gibt es CreateMsgPort()
und DeleteMsgPort()
in der Unit Exec.
program test;
uses
Exec;
var
Mp: PMsgPort;
begin
Mp := CreateMsgPort;
// do something
DeleteMsgPort(Mp);
end.
Falls es nicht genug Speicher gibt um den Messageport anzulegen der Aufruf wird fehlschlagen und ein nil (=0) zurückgeben. Natürlich sollten wir das Ergebnis testen und das Program verlassen falls es ein Problem gibt. Man kann entweder den Messageport mit nil vergleichen oder die spezielle Funktion Assigned()
benutzen.
program test;
uses
Exec;
var
Mp: PMsgPort;
begin
Mp := CreateMsgPort;
if not Assigned(Mp) then
begin
writeln('Failed to create MsgPort');
Exit;
end;
// do something
DeleteMsgPort(Mp);
end.
Dieses Programm testet ob die Erzeugung des Messageports funktioniert hat und wenn nicht gibt es eine Nachricht aus und verlässt das Programm.
IO Request
Für den nächsten Schritt brauchen wir einen IORequest
, praktisch ein Befehl an den Treiber, da wir serielle Kommunikation betreiben wollen benötigen wir eine spezialisierten IORequest namens PIOExtSer
welchen wir in der serial unit finden. (Falls die serial unit nicht gefunden wird, muss das FreePascal aktualisiert werden)
Um so einen IORequest zu erstellen benötigen wir die beiden Funktionen CreateExtIO()
und DeleteExtIO()
aus der exec unit. Wir erweitern unser Programm und erzeugen so ein PIOExtSer
.
program test;
uses
Exec;
var
Mp: PMsgPort;
Io: PIOExtSer;
begin
// create Messageport
Mp := CreateMsgPort;
if not Assigned(Mp) then
begin
writeln('Failed to create MsgPort');
Exit;
end;
// create IO Serial
Io := PIOExtSer(CreateExtIO(mp, SizeOf(TIOExtSer)));
if not Assigned(Io) then
begin
Writeln('Cannot alloc IOExtSer');
Exit;
end;
// do something
DeleteExtIO(PIORequest(Io));
DeleteMsgPort(Mp);
end.
An dieser Stelle möchte ich auf ein kleines Problem hinweisen. Falls der Messageport erfolgreich erzeugt wurde aber der IORequest schlägt fehl, das Programm wird einfach beendet und der Messageport wird nicht wieder zerstört. Man kann geschachtelte if Blöcke benutzen:
program test;
uses
Exec, Serial;
var
Mp: PMsgPort;
Io: PIOExtSer;
begin
// create Messageport
Mp := CreateMsgPort;
if Assigned(Mp) then
begin
// create IO Serial
Io := PIOExtSer(CreateExtIO(mp, SizeOf(TIOExtSer)));
if Assigned(Io) then
begin
// do something
DeleteExtIO(PIORequest(Io));
end
else
begin
Writeln('Cannot alloc IOExtSer');
end;
DeleteMsgPort(Mp);
end
else
begin
writeln('Failed to create MsgPort');
end;
end.
Man kann das so machen, aber wenn es sehr viele Ebenen hat wird es sehr schnell unübersichtlich. I bevorzuge den try...finally..end;
Ansatz oder separate Initialisieren() und Zerstoere() Funktionen, welche bei besonders einfach sind bei Objektorientierter Programmierung da man einen Konstruktor und Destruktor der Klasse hat den man dafür benutzen kann.
Achtung: Wenn man Klassen oder try finally Blöcke benutzen möchte muss man den Compiler im Delphi oder ObjectFPC Modus benutzen. z.B. {$mode objfpc}
.
program test;
{$mode objfpc}
uses
Exec, Serial;
var
Mp: PMsgPort = nil;
Io: PIOExtSer = nil;
begin
try
// create Messageport
Mp := CreateMsgPort;
if not Assigned(Mp) then
begin
writeln('Failed to create MsgPort');
Exit;
end;
// create IO Serial
Io := PIOExtSer(CreateExtIO(mp, SizeOf(TIOExtSer)));
if not Assigned(Io) then
begin
Writeln('Cannot alloc IOExtSer');
Exit;
end;
// do something
finally
if Assigned(Io) then
DeleteExtIO(PIORequest(Io));
if Assigned(Mp) then
DeleteMsgPort(Mp);
end;
end.
Man beachte das alle Variablen mit nil initialisiert werden und dann im finally Teil auf nil getestet werden und nur dann zerstört werden. Diese Funktionen benötigen das eigentlich nicht, da sie intern auch auf nil testen. Aber viele andere Amiga API Funktionen testen nicht auf leere Pointer und stürzen einfach ab. Daher sollte man sich angewöhnen alle Pointer zu testen bevor man sie an Amiga API Funktionen übergibt.
Öffnen des seriellen Devices
Aber zurück zu der seriellen Kommunikation. Es ist alles vorbereitet damit wir das seriellen Sevice öffnen können dafür bietet die exec Unit OpenDevice()
und CloseDevice()
.
var
Res: LongInt;
DevOpen: Boolean = False;
DeviceName: AnsiString = 'serial.device'; // or usbmodem.device
UnitNumber: Integer = 0;
// ...
Res := OpenDevice(PChar(DeviceName), UnitNumber, PIORequest(io), 0);
if Res <> 0 then
begin
Writeln('Unable to open device "' + DeviceName + ' ' + IntToStr(UnitNumber) + '" :' + IntToStr(Res));
Exit;
end;
DevOpen := True;
// ...
if DevOpen then
CloseDevice(PIORequest(io));
Serielle Device Parameter setzen
Bevor wir das Device wirklich benutzen können müssen wir noch einige Parameter setzen, wie Baudrate und Flow control. Dafür müssen wir das erste mal eine Nachricht and den Serielle Device Treiber schicken mit dem IORequest den vorhin erzeugt haben. Zuerst wir konfigurieren den IORequest:
// Parity und XON/XOFF ausschalten
io^.io_SerFlags := (io^.io_SerFlags or SERF_XDISABLED) and (not SERF_PARTY_ON);
io^.io_Baud := Baud; // Baud rate setzen
io^.io_ReadLen := 8; // 8 Bits pro Character
io^.io_WriteLen := 8;
io^.io_StopBits := 1; // 1 Stopbit
io^.IOSer.io_Command := SDCMD_SETPARAMS; // Message Kommando = Setze Parameter
Um die Nachricht zu versenden wir können DoIO()
oder SendIO()
neutzen. DoIO() blockiert bis die Nachricht abgearbeitet ist. SendIO() kehrt sofort zurück, läuft im Hintergrund weiter und wir müssen testen ob die Nachricht beendet ist. I prefer SendIO() because you have more control about what happens and you can break the request if it needs too long time. With CheckIO()
you can test if the request is already finished or use WaitIO() for the end of operation. All together it looks like that:
// Parity und XON/XOFF ausschalten
io^.io_SerFlags := (io^.io_SerFlags or SERF_XDISABLED) and (not SERF_PARTY_ON);
io^.io_Baud := Baud; // Baud rate setzen
io^.io_ReadLen := 8; // 8 Bits pro Character
io^.io_WriteLen := 8;
io^.io_StopBits := 1; // 1 Stopbit
io^.IOSer.io_Command := SDCMD_SETPARAMS; // Message Kommando = Setze Parameter
SendIO(PIORequest(io)); // sende die Nachricht
WaitIO(PIORequest(io)); // Warte auf die Antwort
Text lesen
Um Text vom seriellen Port zu lesen, welches der Arduino sendet, wir müssen wieder eine Nachricht senden mit dem Kommando CMD_READ und einem Puffer wo der Text abgelegt werden soll.
var
Buffer: array[0..256] of char;
// ...
FillChar(Buffer[0], 257, #0); // Buffer löschen (immer ein #0 am Ende)
io^.IOSer.io_Length := 256; // einer weniger als die wirkliche Groesse dahaer immer ein #0 am ende
io^.IOSer.io_Data := @Buffer[0]; // Zeiger auf den ersten wert als Start
io^.IOSer.io_Command := CMD_READ; // Kommando = Lesen
SendIO(PIORequest(io));
WaitIO(PIORequest(io));
// ...
Damit haben wir alles was wir brauchen um den Text vom Arduino zu lesen. Alles zusammen:
program test;
{$mode objfpc}{$H+}
uses
SysUtils,
exec, serial;
const
DefDevice = 'serial.device'; // or usbmodem.device
DefUnit = 0;
DefBaud = 9600;
var
Mp: PMsgPort = nil;
Io: PIOExtSer = nil;
DevOpen: Boolean = False;
Res: LongInt;
DeviceName: string = DefDevice;
UnitNumber: Integer = DefUnit;
Baud: Integer = DefBaud;
Buffer: array[0..256] of char;
begin
try
// create Messageport
Mp := CreateMsgPort;
if not Assigned(Mp) then
begin
writeln('Failed to create MsgPort');
Exit;
end;
// create IO Serial
Io := PIOExtSer(CreateExtIO(mp, SizeOf(TIOExtSer)));
if not Assigned(Io) then
begin
Writeln('Cannot alloc IOExtSer');
Exit;
end;
// Open the device
Res := OpenDevice(PChar(DeviceName), UnitNumber, PIORequest(io), 0);
if Res <> 0 then
begin
Writeln('Unable to open device "' + DeviceName + ' ' + IntToStr(UnitNumber) + '" :' + IntToStr(Res));
Exit;
end;
DevOpen := True;
// configure serial interface
io^.io_SerFlags := (io^.io_SerFlags or SERF_XDISABLED) and (not SERF_PARTY_ON);
io^.io_Baud := Baud;
io^.io_ReadLen := 8; // 8 Bits pro Character
io^.io_WriteLen := 8;
io^.io_StopBits := 1; // 1 Stopbit
io^.IOSer.io_Command := SDCMD_SETPARAMS;
SendIO(PIORequest(io));
WaitIO(PIORequest(io));
// read 256 chars from the serial port
FillChar(Buffer[0], 257, #0);
io^.IOSer.io_Length := 256;
io^.IOSer.io_Data := @Buffer[0];
io^.IOSer.io_Command := CMD_READ;
SendIO(PIORequest(io));
WaitIO(PIORequest(io));
// everything ok - print out the Buffer
writeln('Everything ok, Buffer: ', Buffer);
finally
if DevOpen then
CloseDevice(PIORequest(io));
if Assigned(Io) then
DeleteExtIO(PIORequest(Io));
if Assigned(Mp) then
DeleteMsgPort(Mp);
end;
end.
Wenn man das kompiliert und auf dem Amiga starten mit dem Arduino angeschlossen, man sollte folgendes erhalten:
Work:Sources/Serial> test
Everything ok, Buffer:
Amiga (Msg: 10)
Hello Amiga (Msg: 11)
Hello Amiga (Msg: 12)
Hello Amiga (Msg: 13)
Hello Amiga (Msg: 14)
Hello Amiga (Msg: 15
Work:Sources/Serial>
Wir haben unseren ersten Text vom Arduino. Wie man sehen kann wartet der WaitIO() aufruf bis 256 Zeichen eingetroffen sind. Was aber etwas störend sein kann, wenn man eine direkte Reaktion auf einen Text haben möchte und nicht soviel Text gesendet wird.
EOF Modus
Dafür können wir den EOF Moudus des seriellen Devicetreibers benutzen. Dieser stoppt die Textaufnahme wenn ein "Ende des Files" erhalten wird im Zeichenstrom. Jede Zeile vom Arduino sollte mit einem Return (#13 #10) enden so wir könnten das #10 =($0a) Zeichen benutzen um die Textaufnahme vorzeitig abzubrechen. Um das zu erreichen ändern wir die Parameter die wir am Anfang mir SETPARAM gesetzt haben zu:
// Parity und XON/XOFF ausschalten, EOF anschalten
io^.io_SerFlags := (io^.io_SerFlags or SERF_EOFMODE or SERF_XDISABLED) and (not SERF_PARTY_ON);
io^.io_Baud := Baud; // Baudrate setzen
io^.io_ReadLen := 8; // 8 Bits pro Character
io^.io_WriteLen := 8;
io^.io_StopBits := 1; // 1 Stopbit
io^.io_TermArray.TermArray0 := $0a030303; // Ende des Files Zeichen #10 Rest gefüllt mit $03 = End of Text
io^.io_TermArray.TermArray1 := $03030303;
io^.IOSer.io_Command := SDCMD_SETPARAMS; // Nachtichten Kommando type = Setze Parameter
SendIO(PIORequest(io)); // Sende Nachricht
WaitIO(PIORequest(io)); // Warte auf Antwort
Wenn man das so ändert erhält man schliesslich:
Work:Sources/Serial> serialtest
Everything ok, Buffer: Hello Amiga (Msg: 23)
Work:Sources/Serial>
Das ist schon viel besser, wir erhalten nur eine einzige Zeile des Texts und diese daher auch sofort. Falls der Text länger als 256 Zeichen ist bricht er natürlich immer noch dort ab und man muss nochmal nach dem Rest fragen (oder einen grösseren Buffer bereitstellen).
Aber was passiert, wenn kein Text mehr gesendet wird? Der WaitIO() Aufruf blockiert für immer und man kann ihn auch nicht unterbrechen (ausser mit einem Computerneustart). Um das zu umgehen programmieren wir einen Timeout dazu.
Timeout
Wir brauchen die vorher erwähnte Funktion CheckIO()
um zu prüfen ob das Kommando abgearbeitet wurd und eine Funktion um einen IORequest abzubrechen wenn ein Timeout eintritt, diese Funktion heisst Abort(IO)
. Um die Zeit zu überprüfen kann man sehr einfach die Funktion GetTickCount()
aus SysUtils benutzen, diese gibt uns die Zeit in Millisekunden, welche wir benutzen können um sehr genau Zeitabstände zu messen. Für den Timeout ersetzen wir SendIO()/WaitIO()
für das Kommando CMD_READ mit:
var
t: LongWord;
// ...
t1 := GetTickCount;
repeat
if CheckIO(PIORequest(io)) <> nil then
Break;
Sleep(25);
until GetTickCount - t1 > 10000;
if CheckIO(PIORequest(io)) <> nil then
begin
WaitIO(PIORequest(io));
writeln('Everything ok, Buffer: ', Buffer);
end
else
begin
AbortIO(PIORequest(io));
writeln('Timeout, not enough data: ', Buffer);
end;
Von hier an gibt es nur noch Teile des Sopurce, da es sonst zu lang wird, allerdings kannst du den kompletten Source hier runterladen: Source1.pas[2]