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 :
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 |
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 |
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)
Bei Datenlänge innerhalb des Structs < 255 Byte
| Byte (Gibt die Länge des Datenteils an) | Datenteil |
| Byte (Wert 255) | Word (Gibt die Länge des Datenteils an) | Datenteil |
TYPE
TStruct = class(TObject)
PStruct,
{Zeiger auf den Anfang des Structs}
PData
: pByte; {Zeiger auf den nächsten Bereich im Struct}
sLength : Word;
{Gesammtlänge des Structes}
FUNCTION GetPStruct :
Pointer;
FUNCTION GetDataLength
: Word;
Public
CONSTRUCTOR Create(DataLength:Word);
CONSTRUCTOR CreateFromData(aData
: Pointer);
DESTRUCTOR Destroy;
override;
PROCEDURE
AddData(PWas:Pointer; NewDataLength:Word);
PROCEDURE
GetData(PWas:Pointer; NewDataLength:Word);
Published
property Zeiger
: Pointer read GetPStruct;
property Length
: Word read sLength;
property DataLength :
Word read GetDataLength;
end;
Mit Create wird das Objekt erstellt, der Speicherplatz für den Struct reserviert sowie das Längenbyte bzw. ~word gesetzt. PStruct zeigt auf den Anfang des Structs im Speicher, sLength hält die Gesammtlänge des Structs (incl. Längenbyte/~word) fest.
CONSTRUCTOR TStruct.Create(DataLength:Word);
VAR
ShortLength:^Byte;
LongLength :^Word;
BEGIN
inherited Create;
If DataLength < 255 then begin
GetMem(PStruct,DataLength
+ SizeOf(Byte)); {Speicher allozieren}
PData:=PStruct; Inc(PData,
SizeOf(Byte)); {PData auf Bereich nach Längenbyte stellen}
ShortLength:=Pointer(PStruct);
ShortLength^:=DataLength;
{Längenbyte den Wert zuweisen}
sLength := DataLength
+ SizeOf(Byte); {Gesammtlänge des
Struct festhalten}
end
else begin
GetMem(PStruct,DataLength
+ SizeOf(Byte) + SizeOf(Word));
PData:= PStruct; Inc(PData,
SizeOf(Byte));{PData ein Byte nach Structanfang stellen}
ShortLength:=Pointer(PStruct);
LongLength :=Pointer(PData);
Inc(PData,SizeOf(Word));
{PData auf Bereich nach dem Längenword stellen}
ShortLength^:=255;
{Das "Längenbyte" hat den Wert 255}
LongLength^ :=DataLength;
{Dem nun entscheidenden Längenword den Wert zuweisen}
sLength := DataLength
+ SizeOf(Byte) + SizeOf(Word);
end;
END;
Nun steht der Platz im Speicher bereit, wartet aber noch auf Füllung mit sinnvollen Daten. Dies geschieht durch AddData. Da PData ständig auf den nächsten freien Bereich im Struct zeigt, ist dies recht einfach :
PROCEDURE TStruct.AddData(pWas:Pointer; NewDataLength:Word);
BEGIN
Move(pWas^,PData^,NewDataLength);
{Daten aus übergebenem Speicherbereich in den Struct
kopieren}
Inc(PData, NewDataLength);
{PData auf Bereich nach den neuen Daten stellen}
END;
Anders verhält es sich aber, wenn ein Struct im Speicher vorhanden ist (--> Von der CAPI übergeben worden), dann möchte man schließlich die Daten nacheinander auslesen (vergl. AddData, dort nacheinander schreiben). Die Prozeduren hierfür sind :
CONSTRUCTOR TStruct.CreateFromData(aData :
Pointer);
VAR
ShortLength:^Byte;
LongLength :^Word;
BEGIN
inherited create;
ShortLength:=aData; {Das erste
Byte des Structs --> Länge}
If ShortLength^ < 255 then begin
sLength := ShortLength^
+ SizeOf(Byte); {Gersammtlänge festhalten}
GetMem(PStruct, sLength);
{Speicher allozieren}
Move(aData^,PStruct^,sLength);{Den
Struct aus dem übergebenen Speicherbereich in den
eigenen kopieren}
PData := PStruct; Inc(PData,
SizeOf(Byte)); {vergl. Create}
end
else begin
{Das folgende dürfte jetzt klar sein}
LongLength:=aData;
sLength := LongLength^
+ SizeOf(Byte) + SizeOf(Word);
GetMem(PStruct, sLength);
Move(aData^,PStruct^,sLength);
PData:=PStruct; Inc(PData,
SizeOf(Byte)+SizeOf(Word));
end;
END;
PROCEDURE TStruct.GetData(pWas : Pointer;
NewDataLength:Word);{Wie AddData, nur andersrum}
BEGIN
Move(PData^,pWas^,NewDataLength);
Inc(PData, NewDataLength);
END;
Der Destructor sollte natürlich nicht vergessen werden, um den allozierten Speicherplatz wieder freizugeben :
DESTRUCTOR TStruct.Destroy;
BEGIN
FreeMem(PStruct,sLength);
inherited Destroy;
END;
Nun sind noch die Property des Objektes, welche noch folgende Funktionen benötigen:
FUNCTION TStruct.GetPStruct:Pointer; {Ich
hoffe selbsterkärend}
BEGIN
GetpStruct := Pointer(PStruct);
END;
FUNCTION TStruct.GetDataLength : Word;
{Gibt die Datenlänge innerhalb des Structs
zurück}
VAR
ShortLength:^Byte;
LongLength :^Word;
PHelp
:pByte;
BEGIN
PHelp:=PStruct;
ShortLength:=Pointer(PHelp); {Das
erste Byte des Structes}
If ShortLength^<255 then
GetDataLength:= ShortLength^
else begin
Inc(PHelp,SizeOf(Byte));
LongLength:=Pointer(PHelp);
{Das LängenWord}
GetDataLength:=LongLength^;
end;
END;
Mit diesem Objekt kann man einen Struct (meiner unbedeutenden Meinung nach sogar recht komfortabel) aufbauen, übergeben und Daten auslesen.
Man kann einen Header bilden, indem man einen leeren Header durch
Create erzeugen und dann die Propertys festlegt.
Oder man kann durch CreateFromData einen Header erzeugen und die Propertys
dann auslesen.
Die einzelnen Funktionen gebe ich hier nicht explizit an, diese sind
einfach und können im Quelltext nachgeschaut werden.
UNIT Header;
INTERFACE
USES
Windows;
TYPE
THeader = class(TObject)
PHeader
: pByte;
{Zeiger auf den Header}
FUNCTION GetTotalLength:Word;
{Funktionen für die Propertys}
FUNCTION GetAppID
:Word;
FUNCTION GetCommand
:byte;
FUNCTION GetSubCommand
:byte;
FUNCTION GetMessageNr
:Word;
PROCEDURE SetTotalLength(TotalLength:Word);
PROCEDURE SetAppId(AppID:Word);
PROCEDURE SetCommand(Command:Byte);
PROCEDURE SetSubCommand(SubCommand:Byte);
PROCEDURE SetMessageNr(MessageNr:Word);
FUNCTION GetSpaceInMem:Word;
FUNCTION GetDataPtr
:Pointer;
public
CONSTRUCTOR Create;
CONSTRUCTOR CreateFromData(PData:Pointer);
DESTRUCTOR Destroy; override;
published
property TotalLength
: Word read GetTotalLength write SetTotalLength;
{Die Länge der Nachricht}
property AppID
: Word read GetAppID write SetAppID;
{Die Appl-ID der Anwendung}
property Command
: byte read GetCommand write SetCommand;
{Command der Messages}
property SubCommand
: byte read GetSubCommand write SetSubCommand;
{SubCommand der Message}
property MessageNumber:
Word read GetMessageNr write SetMessageNr;
{Nr der Message}
property Space : Word
read GetSpaceInMem;
{Speicherplatz, den der Header belegt (8 Byte)}
property Data :
Pointer read GetDataPtr;
{Zeiger auf den Header}
end;
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.
|