Willkommen lieber Delphi-Programmierer
mache ruhig Rast auf dieser Seite, aber bedenke: Niemand ist perfekt, jeder Mensch macht Fehler, niederschmetternde Kritik mag noch so befreiend sein, laß es (bitte) lieber ;-)
Also Kommentare, Verbesserungsvorschläge, Kritik, Fragen oder Ähnliches bitte
einfach in mein Gästebuch !

Direkt zur Download-Area

Implementierung einer ISDN-Anrufmonitor-Komponente unter Delphi 3

Der ISDN-Anschluß ist endlich freigeschaltet. Toll, aber nur zu dumm, daß man (an der Hausanlage angeschlossen) immer noch ein analoges Telefon im Zimmer stehen hat. Denn schließlich möchte man die Vorteile der modernen Technik nutzen und schon vor dem Abnehmen die Identität des Gegenüber wissen. Und wen juckt es dann nicht in den Fingern, mal schnell einen Anrufmonitor zu programmieren ?

Doch wie mache ich das ?

Um die Programmierung ISDN-Karten-Unabhängig zu machen wurde von der CAPI-Association ein Standard ins Leben gerufen :

Die CAPI
Die CAPI müßte bei jeder ISDN-Karte in Form einer DLL bereitstehen. Es gibt normalerweise 3 verschiedene DLL's :

Da wir modern sind und Delphi 3 (also 32-Bit) programmieren brauchen wird die Capi2032. Sollte diese DLL auf Ihrem Rechner fehlen wäre ein baldiger Besuch auf der Homepage des Herstellers der ISDN-Karte zu empfehlen !

Diese DLL stellt nun die notwendigen Funktionen bereit, um mit der ISDN-Karte zu kommunizieren. Die einzelnen Funktionen und deren Aufrufkonventionen sind in der Spezifikation beschrieben (Stehen als pdf oder Word - Datein auf der BinTec Homepage zum Download bereit). Dies ist ein lebenswichtiges Dokument für den ISDN-Programmierer !

Doch nun frisch ans Werk, zuallererst natürlich

Die DLL laden

Man könnte die Funktionen natürlich als external mit Bezug auf die DLL deklarieren, damit würde ein Programm aber überhaupt nicht mehr starten, wenn die DLL nicht vorhanden ist. Deswegen wähle ich den anderen Weg :
Die Funktionen werden dynamisch geladen. Dies erledigt meine Unit DLLLoad :

UNIT dllLoad;
INTERFACE
USES
  Windows,Dialogs,Messages,SysUtils;

TYPE
  TCapi_Installed       = FUNCTION : DWord; StdCall;
  TCapi_GetManufacturer = PROCEDURE (SzBuffer : pChar); StdCall;
  TCapi_GetSerialNumber = FUNCTION (SzBuffer : pChar) : DWord; StdCall;
  TCapi_GetVersion      = FUNCTION (var CapiMajor, CapiMinor, ManufacturerMajor,
                                    ManufacturerMinor : dWord) : DWord; StdCall;
  TCapi_GetProfile      = FUNCTION (pProfile : pointer; CtrlNr : DWord) : DWord; StdCall;
  TCapi_Register        = FUNCTION (MsgBufferSize, MaxLogConn, MaxBDataBlocks,
                                    MaxBDataLen : DWord; pApplId : Pointer) : DWord; StdCall;
  TCapi_Release         = FUNCTION (ApplId : DWord) : DWord; StdCall;
  TCapi_PutMessage      = FUNCTION (ApplId : DWord; pCapiMessage : pointer) : DWord; StdCall;
  TCapi_GetMessage      = FUNCTION (ApplId : DWord; var pCapiMessage : pointer)
                                    : DWord; StdCall;
  TCapi_WaitForSignal   = FUNCTION (ApplId : DWord) : DWord; StdCall;

Dies sind nun die unterstützten Funktionen (im Syntax den die CAPI vorschreibt). Das Wörtchen StdCall darf auf keinen Fall vergessen werden, da die Aufrufe den C-Konventionen folgen müssen, und die Welt dort sieht etwas anders aus.
Nun müssen diese Funktionen geladen werden. Man erzeugt zuerst einen Handle auf die Capi und kann dann mit der Delphi-Funktion GetProcAddress die Funktionen "laden". Sobald ich die Funktion CAPI_Installed geladen habe (das setzt voraus, daß die DLL vorhanden ist), führe ich diese auch sofort aus, da sie mir anzeigt ob eine CAPI auf Basis einer vorhandenen ISDN-Karte vorhanden ist, und wenn nicht kann ich's mir sparen, die restlichen Funktionen zu laden.

VAR
 Capi_Installed         : TCapi_Installed;
 Capi_GetManufacturer   : TCapi_GetManufacturer;
 Capi_GetSerialNumber   : TCapi_GetSerialNumber;
 Capi_GetVersion        : TCapi_GetVersion;
 Capi_GetProfile        : TCapi_GetProfile;
 Capi_Register          : TCapi_Register;
 Capi_Release           : TCapi_Release;
 Capi_PutMessage        : TCapi_PutMessage;
 Capi_GetMessage        : TCapi_GetMessage;
 Capi_WaitForSignal     : TCapi_WaitForSignal;

FUNCTION InitCapiDLL:Boolean;   {Die Funktion die die CAPI-Funktionen lädt}
FUNCTION CapiDLLLoaded:Boolean; {Gibt zurück, ob die Funktionen schon geladen sind}

IMPLEMENTATION

CONST
  Capi='capi2032.dll';

VAR
  CapiDLLLoad   : Boolean; {Hält fest, ob die DLL-Funktionen schon geladen sind}
  CapiLibHandle : THandle;

TYPE
  CapiLoadError =class(Exception);

FUNCTION InitCapiDLL:Boolean;
BEGIN
  TRY
    InitCapiDll :=True;
    CapiDLLLoad := True;
    CapiLibHandle := LoadLibrary(Capi); {Handle auf die CAPI-DLL}
    if CapiLibHandle = 0 then  {Is was schiefgelaufen}
      raise CapiLoadError.Create('Bibliothek "CAPI2032.DLL" nicht gefunden.');
    @Capi_Installed := GetProcAddress(CapiLibHandle,'CAPI_INSTALLED');
    if Capi_Installed <> 0 then
      raise CapiLoadError.Create('32-Bit Capi nicht installiert !');
    @Capi_GetManufacturer := GetProcAddress(CapiLibHandle,'CAPI_GET_MANUFACTURER');
    @Capi_GetSerialNumber := GetProcAddress(CapiLibHandle,'CAPI_GET_SERIAL_NUMBER');
    @Capi_GetVersion      := GetProcAddress(CapiLibHandle,'CAPI_GET_VERSION');
    @Capi_GetProfile      := GetProcAddress(CapiLibHandle,'CAPI_GET_PROFILE');
    @Capi_Register        := GetProcAddress(CapiLibHandle,'CAPI_REGISTER');
    @Capi_Release         := GetProcAddress(CapiLibHandle,'CAPI_RELEASE');
    @Capi_PutMessage      := GetProcAddress(CapiLibHandle,'CAPI_PUT_MESSAGE');
    @Capi_GetMessage      := GetProcAddress(CapiLibHandle,'CAPI_GET_MESSAGE');
    @Capi_WaitForSignal   := GetProcAddress(CapiLibHandle,'CAPI_WAIT_FOR_SIGNAL');
  except
    On Error : CapiLoadError do begin
      MessageDlg(Error.Message, mtError, [mbOK], 0);
      CapiDLLLoad  := False;
      InitCapiDLL := False;
    end;
  end;
END;

FUNCTION CapiDLLLoaded:Boolean; {Sag ich nix zu !}
BEGIN
  CapiDLLLoaded:=CapiDLLLoad;
END;

INITIALIZATION
  CapiDLLLoad := False; {Initialisierung (am Anfang ist die DLL noch nicht geladen)}
FINALIZATION
  If CapiDLLLoad then FreeLibrary(CapiLibHandle); {Wenn geladen worden, dann Handle freigeben}
END.

Damit kann ich mit einer Funktion die CAPI-Funktionen laden und sie stehen mir zur Verfügung. Soweit zum lustigen Teil.

An- & Abmelden von der CAPI

Um überhaupt mit der CAPI kommunizieren zu können muß man sich als erstes Anmelden. Hierfür ist die Funktion Capi_Register da. Die Parameter sind so wie unten angegeben zu setzen, dies reicht für einen Anrufmonitor aus, nur bei Datenübertragungen o.ä. müßte man sich hir Gedanken machen. Die Parameter im Einzelnen :
 
Default-Wert
Größe des Nachrichten-Speicher 2048
Maximale Anzahl logischer Verbindungen 1
Maximale Anzahl von Datenblöcken 2
Größe dieser Datenblöcke 2048
Application ID (Eindeutige Nummer der Anwendung, mit der sie sich bei der CAPI identifizieren kann) Wird von der CAPI zurückgegeben
Ein Aufruf könnte also folgendermasen erfolgen :

VAR Error,AppID : Word
...
Error := CAPI_Register(2048,1,2,2048,@AppID);

Ist Error = 0 dann ist alles glatt gegangen sonst ist ein Fehler aufgetreten (welcher ist in der Spezifikation nachzuschauen). In AppID hat man eine ID-Nummer gelifert bekommen, welche bei allen nachfolgenden Kommunikationen mit der CAPI anzugeben ist.

Die Abmeldung ist ganz einfach :

Error := CAPI_Release(AppID);

Damit ist schon einmal der Grundstein einer Kommunikation mit der CAPI gelegt.

CAPI-Messages Senden / Abholen

Da wir einen Anrufmonitor schreiben wollen müssen wir von der CAPI über eingehende Anrufe informiert werden. Damit die CAPI aber weiß, das wir an Anrufen interessiert sind müssen wir zuerst ein Listen_Request senden. Toll, aber es gibt doch gar keine CAPI-Funktion des Namens Listen_Request. Richtig ! Fast sämtliche Kommunikation wird über Messages abgewickelt. Es wird dazu ein Zeiger auf die zu übermittelnde Nachricht mit CAPI_PutMessage an die CAPI übergeben, bzw ein Zeiger auf eine bekommene Nachricht von CAPI_GetMessage übermittelt. Mögliche Aufrufe (nach der Anmeldung bei der CAPI) wären also:

VAR
  Error     : Word;
  AppID     : Word;
  Nachricht : Pointer;
[...]
  Error := CAPI_PutMessage(AppID,Nachricht); {Der Zeiger muß auf die Nachricht zeigen}
[oder]
  Error := CAPI_GetMessage(AppID,Nachricht); {Der Zeiger zeigt danach auf die Nachricht}

Intersessant. Doch was sind diese Messages und wie bilde ich Sie ?

CAPI-Messages

Capi-Messages lassen sich dummerweise in keine feste (vorgegebene) Struktur von Delphi (oder irgend einer anderen Sprache) quetschen. Es ist hier auf jeden Fall etwas Handarbeit angesagt.

Jede Nachricht ist nun ersteinmal so aufgebaut :
 
Byte 1 + Byte 2 Byte 2 +Byte 3 Byte 4 Byte 5 Byte 6 + Byte 7 Byte 8.. ?
Gesammtlänge der Nachricht AppID Command Sub-Command Nachrichtennummer Datenteil der Nachricht
Durch Command und SubCommand ist die Messages festgelegt (z.B. LISTEN_REQ: Command  $05; SubCommand $80).
Im Datenteil der Nachricht liegen die erwarteteten Parameter (s. Spezifikation) nun hintereinander im Speicher. Das Word am Anfang (Länge der Nachricht) gibt die Gesammtlänge (=Gesammtgröße) (--> abgebildete Header + Datenteil) der Nachricht in Bytes an. Soweit so einfach.

Nun kommt es aber leider vor, daß im Datenteil nicht nur Bytes, Words, oder DWords ihr Unwesen treiben. Es gibt auch unzählige, von der Spezifikation als Structs bezeichnete Parameter (Sind einzeln in der Spezifikation beschrieben)
 

 
Byte (Gibt die Länge des Datenteils an) Datenteil
   
Byte (Wert 255) Word (Gibt die Länge des Datenteils an) Datenteil
  Um nun eine Nachricht aufzubauen, ist nicht nur der Datenteil, sondern auch der Header (oben abgebildet) erforderlich. Auch ihn habe ich als Objekt implementiert : Nun hat man alle Handwerkszeuge um eine Nachricht zu erstellen. Und, wer hat's erraten, auch Capi-Messages habe ich als Objekt implementiert. Die Message-Bildung hat ähnlichkeit mit der Bildung eines Structs (Create,AddData; CreateFromData,GetData...).

UNIT Nachr;
INTERFACE
USES
  Windows, Header;

TYPE
  TCapiMessage = class(TObject)
    PData,
    PMessage      : pByte;
    Header        : THeader;
    MessageLength : Word;
    FUNCTION GetPData:Pointer;
  public
    CONSTRUCTOR Create;
    CONSTRUCTOR CreateFromData(aData : Pointer);
    DESTRUCTOR  Destroy; override;
    PROCEDURE   AddData(PNewData:Pointer; NewDataLength:Word);
    PROCEDURE   CompleteMsg(AppID:Word;Command,SubCommand:Byte;MessageNr:Word);
    PROCEDURE   GetData(aData : Pointer; DataLength:Word);
    FUNCTION    GetPNextData(DataLength:Word):Pointer;
  published
    property Nachricht : Pointer read GetPData;
  end;

Soll also eine neue Nachricht gebildet werden, wird der Constructor Create aufgerufen, dieser erzeugt gleich einen Header, alloziert Speicher für die Message (allerdings erstmal nur soviel, wie der Header allein brauchen würde).

CONSTRUCTOR TCapiMessage.Create;
BEGIN
  inherited Create;
  Header:=THeader.Create;       {Neuen (leeren) Header erzeugen}
  GetMem(PMessage,Header.Space);{Speicher allozieren}
  MessageLength:=Header.Space;  {Die Größe des bis jetzt allozierten Speicher festhalten}
END;

Dann werden mit der Prozedure AddData nacheinander die Daten (praktisch die Parameter der Message) in den Speicher der Message geschrieben, die Länge (-> Größe) der Message wird simultan erhöht und jedesmal praktisch Speicher  "dazualloziert".

PROCEDURE TCapiMessage.AddData(PNewData:Pointer; NewDataLength:Word);
VAR
  PHelp,PHelpData : pByte;
BEGIN
  GetMem(PHelp,MessageLength+NewDataLength); {Neuen (größeren) Hilfs-Speicher allozieren}
  Move(PMessage^,PHelp^,MessageLength);{Alten Speicherinhalt an den Anfang des Neuen kopieren}
  FreeMem(PMessage,MessageLength);     {Alten Speicher freigeben}
  PHelpData:=PHelp; Inc(PHelpData,MessageLength);{PHelpData an das Ende des kopierten
                                                  Speicherinhalts im neuen Speicher setzen}
  Move(PNewData^,PHelpData^,NewDataLength);      {um die neuen Daten anzufügen}
  Inc(MessageLength,NewDataLength);     {Die neu Länge der Nachricht errechnen}
  GetMem(PMessage,MessageLength);       {Speicher allozieren}
  Move(PHelp^,PMessage^,MessageLength); {Inhalt aus dem Hilfsspeicher in den Speicher kopieren}
  FreeMem(PHelp,MessageLength);         {Den Hilfsspeicher freigeben}
END;

Nun muß (nachdem alle Daten angefügt wurden) nur noch der Header mit den richtigen Daten gefüllt und im Speicher vor den Datenteil gebracht werden. Dies erledigt die Prozedur CompleteMsg :

PROCEDURE TCapiMessage.CompleteMsg(AppID:Word; Command,SubCommand:Byte; MessageNr:Word);
BEGIN
  Header.TotalLength:=MessageLength; {Header-Daten füllen}
  Header.AppID:=AppID;
  Header.Command:=Command;
  Header.SubCommand:=SubCommand;
  Header.MessageNumber:=MessageNr;
  Move(Header.Data^,PMessage^,Header.Space); {Header im Speicher vor den Datenteil setzen}
END;

Damit können wir schön Messages an die CAPI erzeugen, was aber, wenn man eine Message von der Capi erhält (bzw. einen Zeiger darauf), na, schon erraten ? Natürlich müssen wir auch dafür Funktionen implementieren.

CreateFromData kopiert die Nachricht aus dem Speicher der CAPI in den eigenen (ganz frisch allozierten) und erstellt gleich den Header :

CONSTRUCTOR TCapiMessage.CreateFromData(aData:Pointer);
BEGIN
  inherited Create;
  Header:=THeader.CreateFromData(aData);     {Header (mit Daten) erzeugen}
  GetMem(PMessage,Header.TotalLength);       {benötigten Speicher allozieren}
  Move(aData^,PMessage^,Header.TotalLength); {Die Nachricht in den eigenen Speicher kopieren}
  PData := PMessage; Inc(PData, Header.Space);{PData an den Anfang des Datenteils stellen}
END;

Dann werden zwei Funktionen implementiert, die eine gibt Daten bestimmter Länge zurück (falls dort z.B. ein Word steht) oder ein Zeiger auf die nächsten noch nicht ausgelesenen Daten (falls dort ein Struct steht).

PROCEDURE TCapiMessage.GetData(aData : Pointer; DataLength:Word);
BEGIN
  Move(PData^,aData^,DataLength); {Speicherinhalt kopieren}
  Inc(PData,DataLength); {PData an das Ende des gerade ausgelesenen Speicherbereichs stellen}
END;

FUNCTION TCapiMessage.GetPNextData(DataLength:Word):Pointer;
BEGIN
  GetPNextData := Pointer(PData); {Liefert einen Zeiger auf die nächste "unausgelesene" Stelle}
  Inc(PData,DataLength);          {Zählt den Zeiger PData trotzdem hoch (schließlich sind die
                                   Daten ja trotzdem ausgelsen)}
END;

Damit habe ich auf jeden Fall (egal ob ich an die CAPI oder die CAPI an mich sendet) die Möglichkeit eine Message aufzubauen. Über das Property Nachricht kann der Zeiger auf diese Nachricht ermittelt werden.
Jede Nachricht sollte natürlich nach gebrauch wieder freigegeben werden, womit noch der Hinweis folgen soll, daß in der Destroy Methode nicht nur der Speicher sonder auch der Header wieder freigegeben werden muß :

DESTRUCTOR TCapiMessage.Destroy;
BEGIN
  FreeMem(PMessage,MessageLength); {Speicher freigeben}
  Header.Free;                     {Header freigeben}
  inherited Destroy;
END;

Nun können wir die Message LISTEN_REQUEST einfach so implementieren (Aufbau der Nachricht ist in der Spezifikation beschrieben):

VAR
  Error                  : DWord;
  HelpByte               : Byte;
  CallingPartyNumber,
  CallingPartySubaddress : TStruct;
  CapiMsg                : TCapiMessage;
CONST
  Controller : DWord = 1;               {Default - Werte}
  InfoMask   : DWord = $007F;
  CipMask    : DWord = $1FFF03FE;
  CipMask2   : DWord = 0;
BEGIN
[...]
  CapiMsg:=TCapiMessage.Create;
  CapiMsg.AddData(@Controller,SizeOf(DWord));
  CapiMsg.AddData(@InfoMask  ,SizeOf(DWord));
  CapiMsg.AddData(@CipMask   ,SizeOf(DWord));
  CapiMsg.AddData(@CipMask2  ,SizeOf(DWord));

    CallingPartyNumber:=TStruct.Create(2*SizeOf(Byte));
    HelpByte:=0;
    CallingPartyNumber.AddData(@HelpByte,SizeOf(Byte));
    HelpByte:=$80;
    CallingPartyNumber.AddData(@HelpByte,SizeOf(Byte));

    CallingPartySubAddress:=TStruct.Create(SizeOf(Byte));
    HelpByte:=$80;
    CallingPartySubAddress.AddData(@HelpByte,SizeOf(Byte));

  CapiMsg.AddData(CallingPartyNumber.Zeiger,CallingPartyNumber.Length);
  CapiMsg.AddData(CallingPartySubAddress.Zeiger,CallingPartySubAddress.Length);

    CallingPartyNumber.Free;
    CallingPartySubAddress.Free;

  CapiMsg.CompleteMsg([AppID],$05[LISTEN],$80[REQUEST],[Eindeutige Nachrichtennummer]);

  Error:=Capi_PutMessage([AppID],CapiMsg.Nachricht);
 
Abholen der Nachrichten

So, wer sich jetzt freut, und denkt wir können locker Nachrichten abholen, sie auslesen und sind fertig ist leider etwas auf dem Holzweg. Den das große Problem jetzt ist, wann weiß ich das die CAPI eine Message schickt, die ich dann mit CAPI_GetMessage abholen kann ? Der Aufruf von CAPI_GetMessage kehrt auch sofort zurück wenn keine Message von der CAPI an die Anwendung vorliegt. Polling, ist eine zu unschöne und rechenintensive Sache, von der eindeutig abzuraten ist.
Aber es gibt noch die Funktion CAPI_WaitForSignal, die die Ausführung des Codes solange unterbricht, bis eine Message eingetrudelt ist. Aber Moment, wenn sie die Ausführung des Codes unterbricht, dann können doch keine Events mehr abgearbeitet werden. Richtig ! Die Anwendung würde sich doch recht komisch verhalten, genauer gesagt gar nicht, was zur Folge hätte, daß man entweder den Task  abschießt oder sich jedesmal selbst anruft um die Anwendung zu beenden.
Was tun also. Die Lösung bietet sich darin, einen eigenen Thread zu erzeugen, der diese Funktion CAPI_WaitForSignal aufruft und sobald eine Nachricht vorliegt ein Event des Hauptprogrammes aufruft.

Also auf denn, schnell mal ein Thread programmiert, dem man eine Prozedur übergeben kann, welche beim Ankommen einer Message ausgeführt werden soll. Außerdem kann man den Zeiger auf diese Message aus einem Property lesen, welches nach dem Auslesen des Wertes auf nil gesetzt wird, so kann ich verhindern, daß Nachrichten überschrieben werden.

UNIT Thread;
INTERFACE
USES
  Classes,dllLoad;

TYPE
  TGetMessage = Procedure of object;

  Polling = class(TThread)
  private
    GetMessage : TGetMessage; {Die vom Hauptprogramm übergebene Prozedur, die bei eintreffen
                               einer Message ausgeführt werden soll}
    ApplId     : Word;
    PMessage   : Pointer;     {Der Zeiger auf die empfangene Nachricht}
    FUNCTION GetPMessage : Pointer;
  protected
    PROCEDURE Execute; override;
  public
    CONSTRUCTOR Create(Prozedur : TGetMessage; AppID:Word); {Thread erzeugen}
  published
    property CapiMessage : Pointer  read GetPMessage;
  end;

IMPLEMENTATION

CONSTRUCTOR Polling.Create(Prozedur : TGetMessage; AppID:Word);
BEGIN
  inherited Create(false);
  FreeOnTerminate := True; {Beim terminieren des Threads wird das Objekt automatisch
                            freigegeben}
  GetMessage := Prozedur;  {Die übergebene Prozedur festhalten}
  ApplID := AppID;
  PMessage := nil;         {da (noch) keine Nachricht da}
END;

PROCEDURE Polling.Execute;
VAR
  Return:Word;
BEGIN
  While not Terminated do begin  {Solange der Thread nicht terminiert ist}
    If PMessage = nil then begin {Nur wenn keine Nachricht vorhanden ist, die noch nicht
                                  "abgeholt" wurde}
      Return := Capi_WaitForSignal(ApplId);{Auf Message warten}
      If terminated then break; {Wenn der Thread schon terminiert wurde dann raus hier !}
      If Return = 0 then begin
        Return := Capi_GetMessage(ApplID, PMessage); {Nachricht abholen}
        If Return=0 then          {Wenn dabei alles glatt ging}
          Synchronize(GetMessage) {Die Prozedur des Hauptprogrammes aufrufen}
        else
          PMessage := nil; {Keine Nachricht da (bzw. beim Abholen futsch gegangen)}
      end;
    end;
  end;
END;

FUNCTION Polling.GetPMessage:Pointer; {Wenn das Property für den Zeiger auf die empfanfgene
                                       Message ausgelesen wird}
BEGIN
  GetPMessage := PMessage; {Den Zeiger dem Property zuweisen}
  PMessage := nil;         {Die Nachricht wurde jetzt ja abgeholt}
END;

END.
 
OK, nachdem wir diese Fleißarbeiten endlich hinter uns gebracht haben, können wir an die Arbeit gehen und endlich mal Richtung Anrufmonitor programmieren :

Anrufmonitor-Komponente erstellen

OK, wir brauchen in der Komponente eine Prozedur (à la Dich_Ruf_Ich_Jetzt_Auf_Weil_Ich_Die_Kompo_Nutzen_Will), ein Event der eingehende Anrufe meldet (und ich habe noch ein Event implementiert, der angibt, wann gemeldete Anrufe aufhören [Also wenn's Telefon nimmer klingelt]). Das klingt net schwer, oder? Isses eigentlich auch gar nicht. Als erstes mal bekommt ihr einfach ein Stück Quelltext vorgesetzt :

UNIT ISDNAnrufmonitor;
INTERFACE
USES
  dllLoad,Struct,Nachr,Thread,Classes,Windows,SysUtils,Dialogs,
  Konst; {Konst ist eine Unit, die gewisse Konstanten bereithält; aus dieser Unit verwendete
          Konstanten sind im Quelltext grün}

TYPE
  AnrufKommtProcedure =  {Dieser Prozedurtyp wird als Event für eingehende Nachr. benutzt}
    PROCEDURE (CallingNumber, CalledNumber : String;
               CipValue:Word; CallNumber:Word)of object;
  AnrufEndeProcedure =   {Dieser Prozedurtyp wird als Event für das Ende des Klingelns benutzt}
    PROCEDURE (CallNumber:Word) of object;

  TGetMessage = Procedure of object; {Wird benötigt, da der Thread eine Prozedur dieser Form
                                      übergeben haben will !}

  CapiRegError = class(Exception);   {Tritt en Fehler auf, so benutz ich dies um eine
                                     Exception zu erzeugen}

So dies sind die benötigten Typen, also hau ich euch jetzt gleich die Komponenten-Definition um die Ohren :

  TISDNAnrufmonitor = class(TComponent)
  private
    AppID           : Word;
    MsgNr           : Word;
    IncommingCall   : AnrufKommtProcedure;
    EndeIncommingCall: AnrufEndeProcedure;
    CapiCallBack    : Polling;
    PROCEDURE CapiCalledBack;
    FUNCTION  GetMsgNr : Word;
  public
    PROCEDURE WaitForCall;
  published
    property OnIncommingCall  : AnrufKommtProcedure read IncommingCall     write IncommingCall;
    property EndIncommingCall : AnrufEndeProcedure  read EndeIncommingCall write
                                                         EndeIncommingCall;
  end;
 

Wie hier deutlich zu sehen ist, kann man in der späteren Komponente im Objektinspektor von Delphi die Ereignisse OnIncommingCall (mit den Parametern CallingNumber [Wer ruft an], CalledNumber [An welche MSN geht des ganze], CipValue [gibt verschlüsselt den Dienst an (s. Spezifikation)], CallNumber [eindeutige Nummer, die diesen Anruf identifiziert]) und EndIncommingCall (mit dem Paramter CallNumber [zum Finden, welcher Anruf das ist (Nr wurde von OnIncommingCall ja auch übergeben]). Die Prozeduren, mit denen die Events belegt werden, werden in den privaten Variablen festgehalten.

Mit der Prozedure WaitForCall wird die Komponente "aktiviert", so daß Sie eingehende Anrufe meldet (Nicht erschrecken, ist gar nicht so kompliziert):

VAR {Ja richtig, zählt in der ganzen Unit}
  Registered : Boolean; {Hält fest, ob man (bei der CAPI) angemeldet ist}
  ApplID     : Word;    {Hält die App-ID für die ganze Unit verfügbar}

IMPLEMENTATION

PROCEDURE TISDNAnrufmonitor.WaitForCall;
VAR
  HelpByte               : Byte;
  Error                  : DWord;
  CallingPartyNumber,
  CallingPartySubaddress : TStruct;
  CapiMsg                : TCapiMessage;
BEGIN
  TRY
    If not CapiDLLLoaded then {Wenn DLL noch nicht geladen, dann aber schnell !}
      If not(InitCapiDll) then {Wenn sie nicht geladen werden kann, dann Fehler}
        raise CapiRegError.Create('DLL konnte nicht geladen werden');
    If not Registered then begin {Wenn noch nicht angemeldet}
      Error:=Capi_Register(BufferSize,MaxLogicalConnections,MaxDataBlocks,MaxDataLen,@AppId);
      If Error <> 0 then raise CapiRegError.Create('Registrierung fehgeschlagen ');
      Registered:=True;
      ApplID:=AppID;
    end;
    CapiMsg:=TCapiMessage.Create;   {CAPI-Message erstellen}
    CapiMsg.AddData(@Controller,SizeOf(DWord));
    CapiMsg.AddData(@InfoMask  ,SizeOf(DWord));
    CapiMsg.AddData(@CipMask   ,SizeOf(DWord));
    CapiMsg.AddData(@CipMask2  ,SizeOf(DWord));

      CallingPartyNumber:=TStruct.Create(2*SizeOf(Byte));    {Structs werden als Parameter}
      HelpByte:=0;                                           {benötigt}
      CallingPartyNumber.AddData(@HelpByte,SizeOf(Byte));
      HelpByte:=$80;
      CallingPartyNumber.AddData(@HelpByte,SizeOf(Byte));

      CallingPartySubAddress:=TStruct.Create(SizeOf(Byte));
      HelpByte:=$80;
      CallingPartySubAddress.AddData(@HelpByte,SizeOf(Byte));

    CapiMsg.AddData(CallingPartyNumber.Zeiger,CallingPartyNumber.Length);
    CapiMsg.AddData(CallingPartySubAddress.Zeiger,CallingPartySubAddress.Length);

      CallingPartyNumber.Free;     {Structs werden nicht mehr gebraucht --> Freigeben}
      CallingPartySubAddress.Free;

    CapiMsg.CompleteMsg(AppID,LISTEN,REQ,GetMsgNr); {GetMsgNr wird unten erklärt, gibt die
                                                     nächste Nachrichtennummer zurück}
    Error:=Capi_PutMessage(AppID,CapiMsg.Nachricht); {Nachricht an CAPI (Weg mit dem Ding !!!)}
    If Error <> 0 then  {Is was schiefgelaufen --> Anrufe werden nicht gemeldet !}
      raise CapiRegError.Create('Telefonate können nicht angezeigt werden !');

    CapiCallBack:=Polling.Create(CapiCalledBack,AppId); {Den Thread zum Überwachen, ob neue
                             Nachrichten kommen, erzeugen, und CapiCalledBack als Prozedure
                             angeben, die ausgeführt wird, wenn eine Nachricht vorliegt}
  EXCEPT
    On Error : CapiRegError do
      MessageDlg(Error.Message, mtError, [mbOK], 0);
  END;
END;

So wenn kein Fehler vorkam (Keine Exception ausgelöst wurde), dann weiß die CAPI nun Bescheid, schickt uns zuerst eine Listen_Confirmation, um uns zu sagen, daß Sie verstanden hat. Nett, aber was bringt's uns ? Naja, rein theoretisch könnten wir so erfahren, ob unsere Aktion erfolgreich war oder nicht, aber das schenk ich mir, denn wenn bei WaitForCall alles OK war, dann ist die Nachricht auch richtig angekommen.
Also brauchen wir in CapiCalledBack eigentlich nur auf eine Connect_Indicate Message warten, da diese anzeigt, das ein Anruf reinkommt (und natürlich auf eine Disconnect_Indicate, die anzeigt, das der Anruf vorbei ist [genauer gesagt es nimmer klingelt, sprich der Gegner hat's aufgegeben oder ich nehm am Telefon ab]). Hierzu ist noch zu sagen, daß jede Indicate Message von der CAPI mit einer Response Message beantwortet werden sollte, damit die CAPI den mit der Message verbundenen Speicherplatz wieder freigeben kann.

PROCEDURE TISDNAnrufMonitor.CapiCalledBack;
VAR
  PMessage      : TCapiMessage;
  Info,CipValue : Word;
  PLCI          : DWord;
  CalledNumber,CallingNumber   : TStruct;
  SCalledNumber,SCallingNumber : String;
  HelpByte      : Byte;
  EmptyStruct   : TStruct;
BEGIN
  PMessage:=TCapiMessage.CreateFromData(CapiCallBack.CapiMessage);{Die Nachricht erzeugen
                                                                   (kopieren)}
  If (PMessage.Header.Command = CONNECT) and (PMessage.Header.SubCommand = IND) then begin
    PMessage.GetData(@PLCI, SizeOf(DWord));
    PMessage.GetData(@CipValue, SizeOf(Word));

      CalledNumber := TStruct.CreateFromData(PMessage.GetPNextData(0));{Struct erzeugen}
      PMessage.GetPNextData(CalledNumber.Length);   {Den internen Daten-Zeiger von PMessage
                                                    um die Länge des Structs weiterverschieben}
      CalledNumber.GetData(@HelpByte, SizeOf(Byte));
      SetLength(SCalledNumber,CalledNumber.DataLength - SizeOf(Byte));
      CalledNumber.GetData(PChar(SCalledNumber), CalledNumber.DataLength - SizeOf(Byte));
         {SCalledNumber enthält jetzt die MSN als String}

      CallingNumber := TStruct.CreateFromData(PMessage.GetPNextData(0));{siehe CalledNumber}
      PMessage.GetPNextData(CallingNumber.Length);
      CallingNumber.GetData(@HelpByte, SizeOf(Byte));
      CallingNumber.GetData(@HelpByte, SizeOf(Byte));
      SetLength(SCallingNumber,CallingNumber.DataLength - 2*SizeOf(Byte));
      CallingNumber.GetData(PChar(SCallingNumber), CallingNumber.DataLength - 2*SizeOf(Byte));
         {SCallingNumber enthält jetzt die Nummer des Anrufers (ohne führende 0) als String}
      CalledNumber.Free; {Structs wieder freigeben}
      CallingNumber.Free;

    PMessage.Free; {Nachricht freigeben}

    PMessage:=TCapiMessage.Create; {Neue Nachricht erzeugen}
    PMessage.AddData(@PLCI  ,SizeOf(DWord));
    PMessage.AddData(@Reject,SizeOf(Word));
      EmptyStruct := TStruct.Create(0); {5 lerre Structs einfügen (werden von der CAPI
               erwartet, auch wenn's nix zu übermitteln gibt, aber sonst fehlerhafte Message}
    For HelpByte := 1 to 5 do
      PMessage.AddData(EmptyStruct.Zeiger,EmptyStruct.Length);

    PMessage.CompleteMsg(AppID,CONNECT,RESP,GetMsgNr); {Die CONNECT_IND mit einer CONNECT_RESP}
    CAPI_PutMessage(AppID,PMessage.Nachricht);         {beantworten}

    If Assigned(IncommingCall) then  {Wenn dem Event eine Prozedur zugeordnet ist}
      IncommingCall('0'+SCallingNumber,SCalledNumber,CipValue,PLCI);  {diese ausführen}
  end;

  If (PMessage.Header.Command = DISCONNECT) and (PMessage.Header.SubCommand = IND) then begin
    PMessage.GetData(@PLCI, SizeOf(DWord));
    PMessage.GetData(@Info, SizeOf(Word));

    If Assigned(EndeIncommingCall) then {Wenn dem event was zugewiesen ist}
      EndIncommingCall(PLCI);           {Dann diese Prozedur ausführen}

    PMessage.Free;  {Nachricht freigeben}
    PMessage:=TCapiMessage.Create;

    PMessage.AddData(@PLCI, SizeOf(DWord));   {DISCONNECT_IND mit DISCONNECT_RESP beantworten}
    PMessage.CompleteMsg(AppId,DISCONNECT,RESP,GetMsgNr);
    CAPI_PutMessage(AppID, PMessage.Nachricht);
  end;

  PMessage.Free; {Nachricht freigeben}
END;

OK, rein der Vollständigkeit halber noch die Funktion GetMsgNr :

FUNCTION TISDNAnrufmonitor.GetMsgNr:Word;
BEGIN
  Inc(MsgNr); {Nächste Nachrichtennummer}
  If MsgNr > 100 then MsgNr:=1; {Der Wert 100 ist frei gewählt (aber irgenden Wert muß es
                                schon sein [möglichst <65535])}
  GetMsgNr:=MsgNr;
END;

So, um die Variable Registered nutzen zu können (Siehe WaitForCall) muß Sie mit False initialisiert werden, außerdem sollte nach Gebrauch auch eine Abmeldung von der CAPI erfolgen, deshalb :

INITIALIZATION
  Registered:=False;
FINALIZATION
  If Registered then
    CAPI_Release(ApplID);

So, nun haben wir die Komponente praktisch fertig, es fehlt noch die Prozedur Register, welche die Komponente auf einer Reiterseite in Delphi registriert :

PROCEDURE Register;
[...]
IMPLEMENTATION
[...]
PROCEDURE Register;
Begin
  RegisterComponents('Beispiele', [TISDNAnrufmonitor]);
End;

So, wer es bis hier durchgehalten hat ist echt SPITZE ! Wer das alles verstanden hat, der kann sich meinem größten Respekt sicher sein, und wer jetzt sagt  HÄH, WIE SOLL'N DES GEHN ? der soll mir bitte ne Mail schicken, ich bin überaus dankbar, wenn man mir sagt, was unvollständig, schlecht oder gar unverständlich erklärt wurde, ansonsten viel Spaß damit (Wie wärs mit irgendwas "Mission Impossible" mäßigem ["Incomming Call"-groß auf en Bildschirm, schönen Text und Sound dazu, des ganze mit D-Info koppeln und gleich en Namen zur Nummer suchen ...]. Naja, ihr werdet schon was finden). Mein nächstes Projekt wird sein, alle Freunde und Bekannte zu überreden, ISDN zu beantragen.
 
Download - Area
Die Units (sprich Quelltext) der Komponente qcode.zip (7KB)
Die Komponente (zum direkten benutzen) kompo.zip (31 KB)
Ein kleiner Mini-Anrufmonitor monitor.zip (123 KB)
Der ultimative Fun-Anrufmonitor (Betaphase) funmon.zip (254 KB)
Download - Area



So jetz aber ab zum Gästebuch und was nettes reinschreiben !


Zurück zur Homepage

© 1998 by  Bernd Rücker,   letzte Änderung : 14.05.1998