CAPI-Programmierung

CAPI bedeutet ausgeschrieben "Common ISDN Application Interface" und dient zur Programmierung von ISDN-Karten oder -Adaptern. Ich bemühe mich, das hierbei zu beachtende Prinzip einigermaßen verständlich zu erklären.

Einführung Aufbau eines Anrufes
Quellcode Anrufmonitor (VB 5) [NEU] Quellcode Anruf-Aufbau (VB 5) [NEU]

Im Beispielprojekt capi.zip wurde ein Fehler entdeckt (01.07.2008):
In der Methode "Get_Capi_Message()" wird beim Antworten auf eine DISCONNECT_IND die PLCI falsch gesetzt. Es wird die globale Variable "ConnectInd" statt "DisconnectInd" verwendet:

            With DisconnectResp
                .length = Len(DisconnectResp)
                .Appid = Appid
                .Command = CAPI_DISCONNECT
                .SubCommand = CAPI_RESP
                .Message_Number = &H0
                .PLCI = ConnectInd.PLCI    # muss DisconnectInd.PLCI heißen!
            End With


Einige Links zu anderen Seiten:

http://www.keiffenheim.com/iccm/: Der Capi Call Monitor von Reiner Keiffenheim (Shareware) hat noch sehr viel mehr Funktionen und Komfort als meiner. Bei ihm gibt's auch eine OCX zur CAPI-Abfrage. Meine Kenntnisse stammen auch zum Großteil von Reiner.

http://www.capi.org/document.htm: Die offizielle CAPI-Dokumentation.

http://www.isdn-tools.de/: ISDN und CAPI für Delphi und C++Builder.

http://home.t-online.de/home/Peter.Zwosta/index.htm: Eine Info in Deutsch zur CAPI-Programmierung unter Delphi. Hier sind u.a. auch die Message Strukturen und die Calls beschrieben.

http://welcome.to/capisite/: Wagners CAPI & Teles Homepage mit CAPI-Deklarationen für VB und einer experimentellen OCX.

Einführung in die CAPI-Programmierung

Zurück zur Übersicht ]   [ Aufbau eines Anrufs ]


Um einen Call (Anruf) abzuarbeiten muss der Austausch der Nachrichten (Messages) mit der CAPI wie in der Capi Dokumentation erfolgen, sonst kann das ganze hängen bleiben. (Besonders bei Disconnect_Ind mit Disconnect_Resp antworten).

Beim Messagetausch geht es darum, der Capi auf eine Nachricht (IND=Indication) immer eine Antwort (RESP=Response) zu geben, genauso bekommt man von der Capi auf eine Anfrage (REQ=Request) eine Bestätigung (CONF=Confirmation).

Beispiel: <- Von Capi an Programm
Von Programm an CAPI ->
CAPI_Register ->
Listen_Request ->
<-Connect_Indication
Connect_Response ->
<-Disconnect_Indication
Disconnect_Response ->

Die Deklarationen der verwendeten Funktionen sind im Modul des Anrufmonitor-Beispielprojekts zu finden.

So fängt man z.B. in Form_Load an:

x = CAPI_REGISTER(MessageBuffersize, maxLogicalConnection, _
   maxBDataBlocks, maxBDataLen, Appid)

'Nach dem ListenREQ gibt die CAPI Messages beim Eintreffen
'eines Calls ab.

ListenREQ.length = Len(ListenREQ)
ListenREQ.Appid = Appid
ListenREQ.Command = &H5
ListenREQ.Subcommand = &H80
ListenREQ.Message_Number = 1
ListenREQ.controller = 1
ListenREQ.InfoMask = &H0
ListenREQ.CIPMask = &H1
ListenREQ.CIPMas2 = &H0
ListenREQ.CallingParty = &H0
ListenREQ.CallingPartySub = &H0
x = CAPI_PUT_MESSAGE(ByVal Appid, ListenREQ)


Public Function Get_Capi_Message() 'Hier ein Beispiel um die Message Daten zu laden: 'Es wird eine Message abgerufen und in die Headerstruktur 'kopiert. Ich polle hier mit einem Timer in nachfolgende SUB, da bei 'Verwendung von CAPI_GET_SIGNAL das Programm an der selben Stelle 'stehen bleibt. 'WICHTIG: Dieser Code ist nur ein Ausschnitt aus dem Programm, es fehlen die Antworten ' an die CAPI, ohne die das ganze vermutlich abstürzen würde. ' In meinem Beispielprojekt (siehe oben) ist aber der komplette Code enthalten. ' Wenn Du nicht VB5 hast, kannst Du die Dateien trotzdem im Editor öffnen ' und Dir den Code dort ansehen. x = CAPI_GET_MESSAGE(ByVal Appid, lpCapiBuffer) if x <> 0 Then Exit Function Call RtlMoveMemory(MessageHeader, ByVal lpCapiBuffer, ByVal HeaderLength) 'anhand von MessageHeader.Command und MessageHeader.Subcommand kann man nun 'erkennen, welche Message die Capi geschickt hat. 'Nehmen wir an es ist Command CAPI_CONNECT und Subcommand CAPI_IND, 'dann kann hier die Message wie nachfolgend gelesen werden: Select Case MessageHeader.Command Case CAPI_CONNECT Select Case MessageHeader.Subcommand Case CAPI_IND Call RtlMoveMemory(ConnectInd, ByVal (lpCapiBuffer + HeaderLength), _ ByVal (MessageHeader.length - HeaderLength)) 'In dem ConnectInd.Buffer befinden sich jetzt die Nummern entsprechend 'der Capi Dokumentation strukturiert. Beispiel angerufene Nummer: l1 = Asc(Mid$(ConnectInd.buffer, 1, 1)) 'length of called number If l1 > 0 Then GerufeneNummer$ = Mid$(ConnectInd.buffer, 3, l1 - 1) 'Die anrufende Nummer ist irgendwo (I1+2) dahinter und hat noch ein 'paar zusätzliche Infos: (A$ ist hier die anrufende Nummer) l2 = Asc(Mid$(ConnectInd.buffer, l1 + 2, 1)) If l2 > 0 Then If l2 > 2 Then A$ = Mid$(ConnectInd.buffer, l1 + 5, l2 - 2) Else N = Asc(Mid$(ConnectInd.buffer, l1 + 4,1)) c = Asc(Mid$(ConnectInd.buffer, l1 + 3,1)) If ((N And 32) = 32) And ((N And 64) = 0) Then If ((c And 1) = 1) And ((c And 2) = 0) And _ ((c And 4) = 0) And ((c And 8) = 0) Then A$ = "ISDN Hidden" ElseIf c = 0 Then A$ = "Unknown" Else A$ = "HideCode " + Str$(c) End If End If End If Else A$ = "Error" End If End Select End Select End Sub

Bezüglich der Handshakes mit der Capi hier noch ein paar Erläuterungen:

Es gilt prinzipiell: Du kannst nur REQ's und RESP's an die Capi schicken. Die Capi kann nur IND's und CONF's an Dich schicken. Du bist in diesem Fall Dein Programm. Das sind die sogenannten Subcommands. Es gibt aber noch Commands. Die Commands entsprechen den Klassen und die Subcommands den Properties in der Bill Gates Terminologie. Jede Klasse hat mindestens zwei Properties die einen Handshake ermöglichen.

Eine Klasse ist z.b. LISTEN.
LISTEN hat zwei Properties nämlich Request (REQ) und Confirmation (CONF). Du machst also LISTEN_REQ und die CAPI gibt, wenn alles o.k. ist ein LISTEN_CONF zurück. Sobald dann ein Call reinkommmt, stellt die CAPI ein CONNECT_IND zur Verfügung.

Die CONNECT ist eine weitere Klasse die aber alle 4 Properties (oder Subcommands) hat. Auf die CONNECT_IND musst Du dann mit CONNECT_RESP antworten. In der CONNECT_RESP Struktur ist dann auch vermerkt, was Du mit dem Call anstellen willst.
Nimmst Du den Ruf nicht an, dann schließt der Call nach einiger Zeit und die CAPI meldet DISCONNECT_IND. Darauf hin musst Du zwingend mit DISCONNECT_RESP antworten, da sich nach weiteren versuchen das System aufhängt.

Es gibt noch die Möglichkeit mit ALERT das Beenden des Anrufes zu verhindern, ohne das Gespräch anzunehmen (weiter klingeln lassen), bis jemand anderes am BUS das Gespräch übernimmt oder die Telecom auslöst. Das verwende ich um die weiteren Infos zusätzlich zu der Nummer zu erhalten.

Dies geschieht nach CONNECT_IND mit ALERT_REQ. Die weiteren Infos schickt die Capi dann mit INFO_IND.

Hier nochmals der Ablauf
<------CAPI an Programm
Programm an CAPI

LISTEN_REQ ----->Du willst wissen was auf dem Bus abgeht
<------LISTEN_CONFDie CAPI hat's kapiert und bestätigt
<------CONNECT_INDDie CAPI weiß was und stellt Infos bereit
ALERT_REQ-------->Du hältst die Telekom (und den Anrufer) hin
CONNECT_RESP----->Willst aber nicht abheben
<------ALERT_CONFDie CAPI ist damit einverstanden
<------INFO_INDUnd teilt mit, auf welchen Kanal Du denn abnehmen könntest
INFO_RESP------>Du hast das mitgekriegt und teilst dies der CAPI mit. Und wenn dann jemand abnimmt oder der Anrufer auflegt dann
<-------DISCONNECT_INDmeint die CAPI, du hast die Telekom lange genug hingehalten und gibt Dir noch den Grund des Abbruches mit
DISCONNECT_RESP---->das musst Du unbedingt machen, das ist soviel wie "Auf Wiedersehen"

und danach ist Schluss, die CAPI gibt zu diesem Call leider keinen Mucks mehr von sich.
Und deshalb findest Du auch keine auf CAPI basierenden Monitore, die die Dauer des eingegangenen Anrufes feststellen können.
Auch den Versuch den D-Kanal auf ausgehende Anrufe zu überwachen kannst Du Dir sparen. Es sei denn Du hast die ISDN-Chip Dokumentation und willst im Treiber Level operieren.
Du solltest Dir auf jeden Fall die CAPI Dokumentation von der http://www.capi.org/document.htm herunterladen. Man muss ja nicht alles lesen. Aber einige Teile der Codierung sind nur zu verstehen, wenn man die Tabellen vor Augen hat.

Aufbau eines Anrufs mit der CAPI

Zurück zur Übersicht ]   [ Einführung in die CAPI-Programmierung ]


Fortsetzung CAPI-Programmierung: Aufbau eines Anrufes

Den Quellcode aus diesem Artikel gibt es hier als VB 5-Projekt.
Eine besser funktionierende Alternative, basierend auf meinem Code aber mit einem ByteArray statt fest vorgegebenen Strukturen, hat mir dankenswerterweise Marcus Knuth zur Verfügung gestellt: CopyMem-fuer-CAPI.zip.

Um einen Anruf aufzubauen, müssen folgende Messages mit der CAPI ausgetauscht werden:

CONNECT_REQ ----->Du willst eine Verbindung aufbauen
<------CONNECT_CONFDie CAPI bestätigt dies
<------CONNECT_ACTIVE_INDDie Verbindung wurde aufgebaut, es klingelt
CONNECT_ACTIVE_RESP------->Du bestätigst dies
<-------DISCONNECT_INDDie Verbindung wurde getrennt
DISCONNECT_RESP---->Bestätigung, unbedingt erforderlich!

Die Deklarationen der verwendeten Funktionen sind im Modul des Beispielprojekts zu finden.

In meinem Beispielprojekt muss man zunächst einmal die eigene Rufnummer (MSN) und die anzurufende Nummer eingeben und dann auf "Anrufen" klicken. Dadurch wird folgender Code ausgeführt:

  Connect_Req.Appid = Appid
  Connect_Req.Command = CAPI_CONNECT
  Connect_Req.Subcommand = CAPI_REQ
  Connect_Req.MessageNumber = msgnr
  Connect_Req.Controller = &H1
  Connect_Req.CIP = 1
  Connect_Req.BProt = &H1
  Connect_Req.CalledNumber = Chr$(128) + txtNummer.Text
  Connect_Req.Length = Len(Connect_Req)
  Connect_Req.CalledNumberLen = 12
  Connect_Req.CallingNumber = Chr$(0) + Chr$(128) + txtMSN.Text
  Connect_Req.CallingNumberLen = 8
  Connect_Req.CalledSub = &H0
  Connect_Req.CallingSub = &H0
  Connect_Req.BC = &H0
  Connect_Req.LLC = &H0
  Connect_Req.HLC = &H0
  Connect_Req.AdditionalInfo = &H0
  ret = CAPI_PUT_MESSAGE(ByVal Appid, Connect_Req)

Es werden also einige Default-Werte gesetzt (Appid, Command, Subcommand usw.) und die anzurufende Nummer (Called Number) sowieso die eigene MSN (Calling Number) eingetragen. Die CAPI will bei letzteren beiden auch die Länge des Feldes wissen. Dies ist die Länge, die bei der Deklaration (siehe Beispielprojekt, capi-anruf.bas) festgelegt wurde. Mit diesem Code ist es leider nicht möglich, die Rufnummernlänge beliebig zu erlauben, denn wenn das Feld länger ist als die Rufnummer, wird der Anruf nicht aufgebaut bzw. es wird keine MSN übermittelt. Hier bietet sich der alternative Lösungsansatz von Marcus Knuth (siehe oben) mit einem Byte-Array an.

Wie auch der CAPI-Dokumentation entnehmbar ist, muss man vor die zu wählende Rufnummer ein Chr$(128) setzen, und vor die eigene MSN zusätzlich auch noch ein Chr$(0). Alle nicht benötigten Werte setze ich hier auf &H0, die MessageNummer ist fortlaufend, msgnr wird jedesmal hochgezählt.

Wenn alle Informationen eingetragen sind, werden sie an die CAPI übergeben.

Um die Verbindung wieder zu trennen, wird folgender Code benötigt:

  DisconnectReq.Appid = Appid
  DisconnectReq.Command = CAPI_DISCONNECT
  DisconnectReq.Subcommand = CAPI_REQ
  DisconnectReq.Length = Len(DisconnectReq)
  DisconnectReq.Message_Number = msgnr
  DisconnectReq.PLCI = PLCI
  ret = CAPI_PUT_MESSAGE(ByVal Appid, DisconnectReq)

Die Variable PLCI enthält eine eindeutige Nummer, die den Anruf identifiziert. Diese Nummer wird nach dem Aufbau des Calls von der Capi durch die CONNECT_CONF-Message mitgeteilt. Da der Abbau der Verbindung nicht weiter schwer ist, kommen wir gleich zur Behandlung der CAPI-Messages, die wie im Anrufmonitor in der Sub Get_Capi_Message stattfindet. Ich beschränke mich hier auf Auszüge, das Gesamtkonzept ist der CAPI-Einführung bzw. dem Quellcode zu entnehmen:

  Case CAPI_CONNECT
    If MessageHeader.Subcommand = CAPI_CONF Then 'Antwort der CAPI auf Connect-REQ
       Call RtlMoveMemory(ConnectConf, ByVal (lpCapiBuffer + HeaderLength), _
             ByVal (MessageHeader.Length - HeaderLength))
       PLCI = ConnectConf.PLCI
    End If

Beim CONNECT_CONF (wird von der CAPI nach unserem CONNECT_IND geschickt) bekommen wir also die PLCI und speichern sie zur späteren Verwendung.

Nachdem der Call verarbeitet wurde und es auf der Gegenstelle klingelt, kommt ein CONNECT_ACTIVE_IND:

  Case CAPI_CONNECT_ACTIVE
    If MessageHeader.Subcommand = CAPI_IND Then
      With ConnectActiveResp
         .Appid = Appid
         .Command = CAPI_CONNECT_ACTIVE
         .Subcommand = CAPI_RESP
         .Length = Len(ConnectActiveResp)
         .Message_Number = &H0
         .PLCI = PLCI
      End With
      x = CAPI_PUT_MESSAGE(ByVal Appid, ConnectActiveResp)
    End If

Auch hier benötigt man wieder die PLCI, um korrekt mit einem CONNECT_ACTIVE_RESP antworten zu können.

Wird nun die Verbindung unterbrochen, kommt ein DISCONNECT_IND:

  Case CAPI_DISCONNECT
    If MessageHeader.Subcommand = CAPI_IND Then
       Call RtlMoveMemory(DisconnectInd, ByVal (lpCapiBuffer + HeaderLength), _
             ByVal (MessageHeader.Length - HeaderLength))
       MsgBox "Verbindung wurde beendet: " & Info_Number_Evaluation$(DisconnectInd.Reason)
       With DisconnectResp
         .Length = Len(DisconnectResp)
         .Appid = Appid
         .Command = CAPI_DISCONNECT
         .Subcommand = CAPI_RESP
         .Message_Number = &H0
         .PLCI = PLCI
       End With
        
       x = CAPI_PUT_MESSAGE(ByVal Appid, DisconnectResp)
    End If

Selbes Spiel, antworten mit DISCONNECT_RESP, einsetzen der PLCI zur Identifizierung. Um den Grund der Trennung zu erfahren, wird aus dem übergegebenen Buffer hier noch die Reason ausgelesen und durch die Funktion Info_Number_Evaluation$, die die wichtigsten Gründe enthält, ausgewertet.

Sobald ich weitere Erkenntnisse habe, werde ich diese hier veröffentlichen!