Betreff:             Re: Accessing serial ports with Delphi 3???
     Datum:             Wed, 26 Aug 1998 16:52:03 +0200
       Von:             Carl Smotricz 
     Firma:             FRA AE-FPS/FDL
     Foren:             comp.lang.pascal.delphi.misc

Patrick Echterbruch wrote:
> 
> Hi all!!
> 
> Is there a way to gain access to the serial ports under Delphi 3 ?
> 
> I need to write an application which reads data from and writes to e.g.
> COM1. I've not found any predefined components, nor were there informations
> in the Delphi online-help.
> 
> I'd appreciate you sending me a Mail directly to Echterbruch@td-service.de,
> because i don't have the time to check the newsgroups frequently.
> 
> Thanks in advance
> 
> Patrick Echterbruch

Patrick,

I've recently written a simple Delphi project which makes extensive use
of serial communications. The customer was unwilling to pay and thus the
following code was rotting away in my garbage can until I came across
your post. The comments are in German but I don't think you will have
any problem with that! :)

This program was written for and ran successfully under D1. It
(probably) uses the 16 bit comms API, but ran successfully under NT 4.0
and would probably run under W95 and W98. Also, I am guessing these APIs
are still supported under D2/D3/D4.

Essentially, Delphi is making C-like calls to the Windows API, which
offers a number of functions for sending characters, checking the number
of input characters available and reading those characters. The program
does not do much else, so for simplicity the input here is polled. An
interrupt driven implementation would be more CPU efficient but more
complicated.

The following is not demo code but an actual running program (minus the
form). I've forgotten how it works, so please try to figure it out for
yourself. If you have questions remaining, contact me at 
   'carls@ipf.de'
and I can try to help you further.

re's,

Carl

---------- snip here -----------
unit DeusMain;

interface

uses
  SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
  Forms, Dialogs, StdCtrls, Buttons, Gauges, ExtCtrls, IniFiles,
  DeusImg;

type
  TfrmMain = class(TForm)
    pnlMain: TPanel;
    Balken: TGauge;
    txtData1: TLabel;
    txtData4: TLabel;
    txtData2: TLabel;
    txtData5: TLabel;
    Timer1: TTimer;
    txtData3: TLabel;
    txtData6: TLabel;
    pnlStatus: TPanel;
    pnlStatusLabel: TPanel;
    pnlStatusText: TPanel;
    lblStatusText: TLabel;
    pbxLed: TPaintBox;
    lblTaktText: TLabel;
    lblHeaderLinks: TLabel;
    lblHeaderRechts: TLabel;
    procedure Timer1Timer(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure pbxLedPaint(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  frmMain: TfrmMain;

implementation

{$R *.DFM}

const
  CR = #13;

{ *** FEHLER *** }
const
  FehlerAllesOK = 0;
  FehlerTafelTimeout = 1;
  FehlerTafelKaputt = 2;
  FehlerHostTimeout = 4;

type
  TFehler = class(TObject)
  private
    FStatus: Integer;
  public
    constructor Create;
    procedure displayStatus;
    procedure setTafelTimeout;
    procedure setTafelKaputt;
    procedure setTafelOK;
    procedure setHostTimeout;
    procedure setHostOK;
    property Status: Integer read FStatus;
  end;

{ *** HOST *** }
type
  THostStatus = (HostOK, OpenFehler, HostTimeout);
  THost = class(TObject)
  private
    FStatus: THostStatus;
    procedure setStatus(st: THostStatus);
  public
    property Status: THostStatus read FStatus write setStatus;
  end;

{ *** TAFEL *** }
type
  TTafelStatus = (Gepollt, TafelTimeout, TafelOK, Tafelkaputt);
  TTafel = class(TObject)
  private
    FStatus: TTafelStatus;
    procedure setStatus(st: TTafelStatus);
  public
    Spalte1, Spalte2: String[12];
    property Status: TTafelStatus read FStatus write setStatus;
    procedure setActive(active: Boolean);
    procedure setText(Sp1, Sp2: String);
  end;


var
  Host: THost;
  Tafel: TTafel;
  Fehler: TFehler;

  SettingPort: Integer;     { COMn }
  SettingSpeed: LongInt;    { Baud }
  SettingInterval: Integer; { Sekunden }
  SettingDateiname: String; { Eingabedateiname }
  SettingStation: Byte;     { Tafel-Stationsadresse }

  Takt: Integer;

  Zeit: Integer;
  CommDCB: TDCB;
  CommOffen: Boolean;

procedure SeriellZeichenAusgeben(Zeichen: char); forward;


{ --------------------------------------------------------
  Allgemeine Routinen
  -------------------------------------------------------- }


{ Das Verzeichnis des EXE-Files zurückgeben }
function ExeDir: String;
begin
  ExeDir := ExtractFilePath(Application.ExeName);
end;


{ Daten umwandeln und auf COM-Port ausgeben }
procedure GibDatenAus(var Data1, Data2: String);
var
  GewandelteDaten: String;
  j: Integer;
begin
  GewandelteDaten := Data1 + Data2;
  for j := 1 to Length(GewandelteDaten) do
    SeriellZeichenAusgeben(GewandelteDaten[j]);
end;


{ Die Daten einlesen }
procedure LiesVomHost;
label 1, 2, 3;
var
  Datei: Text;
  VollerDateiName: String;
  OK: Boolean;
  Zeile1, Zeile2: String;
begin
  OK := false;
  VollerDateiName := ExeDir + SettingDateiName;
  if not FileExists(VollerDateiName) then goto 1;
  {$I-}
  AssignFile(Datei, VollerDateiName);
  if IOResult <> 0 then goto 1;
  Reset(Datei);
  if IOResult <> 0 then goto 3;
  if Eof(Datei) then goto 1;
  Readln(Datei, Zeile1);
  if IOResult <> 0 then goto 1;
  if Eof(Datei) then goto 1;
  Readln(Datei, Zeile2);
  if IOResult <> 0 then goto 1;
  { Hier alles OK }
  OK := true;
3: { Fehler-Recovery mit Close und Delete }
  CloseFile(Datei);
  if IOResult <> 0 then goto 2;
2: { Fehler-Recovery mit Delete }
  DeleteFile(VollerDateiName);
  if IOResult <> 0 then goto 1;
1: { Fehler-Recovery ohne Close oder Delete }
  {$I+}
  if not OK then begin
    if Host.Status = HostOK then
      Host.Status := OpenFehler
    else
      Host.Status := HostTimeout;
  end else begin
    { Daten sind OK - jetzt fuer die Tafel aufbereiten }
    Tafel.setText(Zeile1, Zeile2);
    Host.Status := HostOK;
  end;
end;


{ Ein Telegramm vom seriellen Port einlesen }
function SeriellEinlesen: String;
var
  Stat: TComStat;
  NChars: Word;
  Buf: Array[0..200] of Char;
  j: Integer;
  InputStr: String;
begin
  { Versuche, Text vom Port zu bekommen }
  InputStr := '';
  GetCommError(CommDCB.ID, Stat);
  NChars := Stat.cbInQue;
  if NChars > 0 then begin
    NChars := ReadComm(CommDCB.ID, Buf, 200);
    if NChars > 0 then begin
      for j := 1 to NChars do
        InputStr := InputStr + Buf[j-1];
    end; { if }
  end; { if }
  Result := InputStr;
end;


{ Die serielle Schnittstelle oeffnen }
function SeriellOeffnen: Boolean;
const                    { from DOC\WINTYPES.INT        }
  ie_BadID    = (-1);    { Invalid or unsupported id    }
  ie_Open     = (-2);    { Device Already Open          }
  ie_NoPen    = (-3);    { Device Not Open              }
  ie_Memory   = (-4);    { Unable to allocate queues    }
  ie_Default  = (-5);    { Error in default parameters  }
  ie_Hardware = (-10);   { Hardware Not Present         }
  ie_ByteSize = (-11);   { Illegal Byte Size            }
  ie_BaudRate = (-12);   { Unsupported BaudRate         }
var
  CommStat: Integer;
  PortName:  Array[1..20] of char;  { 'COM1' + #0; }
  CommParms: Array[1..50] of char;  { 'COM1:2400,N,8,1' + #0; }
begin
  StrPCopy(@PortName, Format('COM%d', [SettingPort]));
  StrPCopy(@CommParms, Format('COM%d:%d,N,8,1', [SettingPort,
SettingSpeed]));
  { Open COM port }
  CommStat := OpenComm(@PortName, 1024, 1024);
  if CommStat < 0 then begin
    MessageDlg(
      Format('Der Port %s konnte nicht geöffnet werden. ' +
             '(Fehlerstatus: %d)' + CR + CR +
             'Bitte wählen Sie  einen anderen Port oder ' +
             'beheben Sie ggfs. das Hardware-Problem!',
             [StrPas(@PortName), CommStat]),
      mtError, [mbOK], 0);
    Result := False;
  end else begin
    CommStat := BuildCommDCB(@CommParms, CommDCB);
    if CommStat < 0 then begin
      MessageDlg(
        Format('BuildCommDCB Error; String = "%s"', [CommParms]),
        mtError, [mbOK], 0);
      Result := False;
    end;
    CommStat := SetCommState(CommDCB);
    if CommStat < 0 then begin
      MessageDlg(
        Format('SetCommState Error %d', [CommStat]),
        mtError, [mbOK], 0);
      Result := False;
    end else begin
      CommOffen := True;
      Result := True;
    end;
  end;
end; { SeriellOeffnen }


{ Schnittstelle dicht }
procedure SeriellSchliessen;
begin
  if CommDCB.ID <> $FF then
    CloseComm(CommDCB.ID);
end;


{ Ein Zeichen aufs serielle Port ausgeben }
procedure SeriellZeichenAusgeben(Zeichen: char);
var
  Stat: TComStat;
  Buf: Array[0..0] of Char;
begin
  GetCommError(CommDCB.ID, Stat);
  Buf[0] := Zeichen;
  WriteComm(CommDCB.ID, @Buf[0], 1);
end;


{ Takt 0:
  Ggw. Status auf Display ausgeben, Tafel pollen. }
procedure Takt0;
begin
  Fehler.displaystatus;
  Zeit := Zeit + 1;
  if Zeit > SettingInterval then
    Zeit := 0;
  frmMain.Balken.Progress := Zeit;
  { Pollen = Stationsadresse ausgeben mit High Bit }
  SeriellZeichenAusgeben(Char($80 + SettingStation));
  Tafel.Status := Gepollt;
end;


{ Takt 1:
  Lies Daten von der Tafel ein.
  Entweder Kurztelegramm (= OK)
  oder Fehlertelegramm
  oder nix }
procedure Takt1;
var
  Telegramm: String;
begin
  Telegramm := SeriellEinlesen;
  if Telegramm = '' then begin
    Tafel.Status := TafelTimeout;
    Fehler.setTafelTimeout;
  end else if Length(Telegramm) = 1 then begin
    (* Kurztelegramm *)
    Tafel.Status := TafelOK;
    Fehler.setTafelOK;
  end else begin
    (* Fehlertelegramm auswerten *)
    Tafel.Status := TafelKaputt;
    Fehler.setTafelKaputt;
  end;
end;


{ Takt 2:
  1. Versuch, Daten vom Host zu lesen.
  Dies passiert nur, wenn das Intervall genau fertig ist. }
procedure Takt2;
begin
  if Zeit = SettingInterval then
    LiesVomHost;
end;


{ Takt 3:
  2. Versuch, Daten vom Host zu lesen.
  Nur, wenn der 1. Versuch gescheitert war. }
procedure Takt3;
begin
  if (Zeit = SettingInterval) and (Host.Status = OpenFehler) then
    LiesVomHost;
end;


{ Takt 4:
  Dummy-Read of Antwort vom Telegramm (sofern vorhanden) }
procedure Takt4;
begin
  SeriellEinlesen;
end;


{ --------------------------------------------------------
  Routine(n) des Objekts 'Fehler'
  -------------------------------------------------------- }

constructor TFehler.Create;
begin
  inherited Create;
  FStatus := 0;
end;


procedure TFehler.displayStatus;
const
  TempStatus: Integer = 0;
begin
  if TempStatus = 0 then
    TempStatus := Fehler.Status;
  if (TempStatus and FehlerTafelTimeout) > 0 then begin
    frmMain.lblStatusText.Caption := 'Keine Verbindung zur Tafel';
    frmMain.lblStatusText.Color := clRed;
    TempStatus := TempStatus and not FehlerTafelTimeout;
  end else if (TempStatus and FehlerTafelKaputt) > 0 then begin
    frmMain.lblStatusText.Caption := 'Tafel defekt';
    frmMain.lblStatusText.Color := clRed;
    TempStatus := TempStatus and not FehlerTafelKaputt;
  end else if (TempStatus and FehlerHostTimeout) > 0 then begin
    frmMain.lblStatusText.Caption := 'Keine Host-Daten';
    frmMain.lblStatusText.Color := clRed;
    TempStatus := TempStatus and not FehlerHostTimeout;
  end else begin
    frmMain.lblStatusText.Caption := 'OK';
    frmMain.lblStatusText.Color := clLime;
  end;
end;

procedure TFehler.setTafelTimeout;
begin
  FStatus := FStatus or FehlerTafelTimeout;
end;

procedure TFehler.setTafelKaputt;
begin
  FStatus := FStatus or FehlerTafelKaputt;
end;

procedure TFehler.setTafelOK;
begin
  FStatus := FStatus and not (FehlerTafelTimeout or FehlerTafelKaputt);
end;

procedure TFehler.setHostTimeout;
begin
  FStatus := FStatus or FehlerHostTimeout;
end;

procedure TFehler.setHostOK;
begin
  FStatus := FStatus and not FehlerHostTimeout;
end;


{ --------------------------------------------------------
  Routine(n) des Objekts 'Host'
  -------------------------------------------------------- }

{ Ändert den Status des Hosts und sorgt
  dafuer, daß das Display richtig anzeigt }
procedure THost.setStatus(st: THostStatus);
begin
  if (FStatus <> st) then begin
    FStatus := st;
    (* Display richtig aufsetzen *)
    case st of
      HostTimeout: begin
        Fehler.setHostTimeout;
        Tafel.setActive(false);
      end;
      OpenFehler,
      HostOK: begin
        Fehler.setHostOK;
        Tafel.setActive(true);
      end;
    end;
  end;
end;


{ --------------------------------------------------------
  Routine(n) des Objekts 'Tafel'
  -------------------------------------------------------- }

{ Setzt den Tafeltext aktiv oder inaktiv }
procedure TTafel.setActive(active: Boolean);
var
  Farbe: TColor;
begin
  if active then begin
    Farbe := clBlack;
  end else begin
    setText('            ', '            ');
    Farbe := clGray;
  end;

  with frmMain do begin
    txtData1.Color := Farbe;
    txtData2.Color := Farbe;
    txtData3.Color := Farbe;
    txtData4.Color := Farbe;
    txtData5.Color := Farbe;
    txtData6.Color := Farbe;
  end;
end;

{ Ändert den Status der Tafel und sorgt dafuer, daß die LED umschaltet }
procedure TTafel.setStatus(st: TTafelStatus);
begin
  if (FStatus <> st) then begin
    FStatus := st;
    frmMain.pbxLed.invalidate;
  end;
end;

{ Setzt die Tafel-Texte ins Display }
procedure TTafel.setText(Sp1, Sp2: String);
var
  Telegramm: String;
  j: Integer;
begin
  Tafel.Spalte1 := Sp1;
  Tafel.Spalte2 := Sp2;
  { Daten auf Bildschirm abbilden }
  frmMain.txtData1.Caption := Copy(Sp1,2,4);
  frmMain.txtData2.Caption := Copy(Sp1,6,4);
  frmMain.txtData3.Caption := Copy(Sp1,10,4);
  frmMain.txtData4.Caption := Copy(Sp2,2,4);
  frmMain.txtData5.Caption := Copy(Sp2,6,4);
  frmMain.txtData6.Caption := Copy(Sp2,10,4);
  { Daten als Telegramm für Tafel aufbereiten }
  Telegramm := '';
  (* formatieren!! *)
  { ausgeben }
  for j := 1 to Length(Telegramm) do
    SeriellZeichenAusgeben(Telegramm[j]);
end;

{ --------------------------------------------------------
  Die folgenden Routinen haengen an Events, d.h. an
  Windows-Ereignissen. Sie stehen hier in der Reihenfolge,
  in der sie normalerweise aufgerufen werden.
  -------------------------------------------------------- }

{ Wenn die Form erzeugt wird, d.h. beim Programmstart. }
procedure TfrmMain.FormCreate(Sender: TObject);
var
  IniFile: TIniFile;
begin
  IniFile := TIniFile.Create(ChangeFileExt(Application.ExeName,
'.INI'));
  SettingPort := IniFile.ReadInteger('Comm', 'Port', 2);
  SettingSpeed := IniFile.ReadInteger('Comm', 'Speed', 9600);
  SettingStation := IniFile.ReadInteger('Comm', 'Station', 1);
  SettingInterval := IniFile.ReadInteger('Data', 'Interval', 120);
  Top := IniFile.ReadInteger('Window', 'Top', 0);
  Left := IniFile.ReadInteger('Window', 'Left', 0);
  WindowState := TWindowState(IniFile.ReadInteger('Window', 'State',
Ord(wsNORMAL)));
  SettingDateiName := IniFile.ReadString('Data', 'File', 'DATEN.DAT');
  lblHeaderLinks.Caption := IniFile.ReadString('Header', 'Links',
'TAFEL1');
  lblHeaderRechts.Caption := IniFile.ReadString('Header', 'Rechts',
'TAFEL2');
  IniFile.Free;
  CommDCB.ID := $FF;  { keine Schnittstelle aktiv }
  Balken.MaxValue := SettingInterval;
end;


{ Wenn die Form gezeigt wird }
procedure TfrmMain.FormShow(Sender: TObject);
begin
  if not SeriellOeffnen then begin
    Close;
  end;
end;


{ Dies passiert jede 1/5 Sekunde beim Tick des Timers }
procedure TfrmMain.Timer1Timer(Sender: TObject);
begin
  Takt := (Takt + 1) mod 5;
  lblTaktText.Caption := IntToStr(Takt);
  case Takt of
  0: Takt0;
  1: Takt1;
  2: Takt2;
  3: Takt3;
  4: Takt4;
  end;
end;


{ Darstellung der LED }
procedure TfrmMain.pbxLedPaint(Sender: TObject);
  procedure {internal} KopiereBild(nach: TPaintBox; von: TImage);
  var
    bgColor: TColor;
  begin
    bgColor := von.Canvas.Pixels[0,0];
    nach.Canvas.BrushCopy(Rect(0,0,15,15), TBitmap(von.Picture.Graphic),
                          Rect(0,0,15,15), bgColor);
  end;
begin
  case Tafel.Status of
  Gepollt:      KopiereBild(frmMain.pbxLed, frmImages.imgLedAus);
  TafelTimeout: KopiereBild(frmMain.pbxLed, frmImages.imgLedRot);
  TafelOK:      KopiereBild(frmMain.pbxLed, frmImages.imgLedGruen);
  end;
end;

{ Beim Schliessen des Programms }
procedure TfrmMain.FormClose(Sender: TObject; var Action: TCloseAction);
var
  IniFile: TIniFile;
begin
  Timer1.enabled := False;
  SeriellSchliessen;
  IniFile := TIniFile.Create(ChangeFileExt(Application.ExeName,
'.INI'));
  IniFile.WriteInteger('Comm', 'Port', SettingPort);
  IniFile.WriteInteger('Comm', 'Speed', SettingSpeed);
  IniFile.WriteInteger('Comm', 'Station', SettingStation);
  IniFile.WriteInteger('Data', 'Interval', SettingInterval);
  IniFile.WriteString ('Data', 'File', SettingDateiName);
  IniFile.WriteInteger('Window', 'Top', Top);
  IniFile.WriteInteger('Window', 'Left', Left);
  IniFile.WriteInteger('Window', 'State', Ord(WindowState));
  IniFile.WriteString ('Header', 'Links', lblHeaderLinks.Caption);
  IniFile.WriteString ('Header', 'Rechts', lblHeaderRechts.Caption);
  IniFile.Free;
end;

initialization
  Tafel := TTafel.Create;
  Tafel.Status := Gepollt;
  Host := THost.Create;
  Host.Status := HostOK;
  Fehler := TFehler.Create;
end.

--------------------------------
-- 
If the price is right my programming skill is yours.
But unless otherwise stated, my opinions are my own.

    Source: geocities.com/~franzglaser/tpsrc

               ( geocities.com/~franzglaser)