TAPI for the home
Part III - A TAPI 2.0 Wrapper Component

My interest for TAPI was prompted when I decided to develop «home» applications that would provide my computer with dialing, callerID and answering machine capabilities. I decided to develop a TAPI application myself. As a result, in this series of three articles, I will review the basics of TAPI, devise a component, develop the recording and playback capabilities for the modem and develop an In-process automation server called gtroTelServer that would provide the required capabilities to calling applications. This second article documents the development of the TGtroTAPI component [the othertwo articles are in preparation.

Now that we know everything we need to know about TAPI (see Part I - Basics of TAPI) and that we have components that can play/record sound to the modem (see Part II - Two components to play/record sound with a voice modem), we can develop a component specially designed for the home which provides the basic functionalities of TAPI, i.e., making outbound calls, CallerID and minimal voice recording capabilities.

As this is a Delphi component, Object-Oriented Programming (OOP) needs to be used. First, I will declare the class TGtroCustomTAPI class as a descendant of TComponent. following the way most classes are declared in the VCL. It allows me to give properties of this class minimum visibility in this class The resulting class that I will use in the applications is TGtroTAPI which is declared as a descendant of TGtroCustomTAPI in which I give variables, properties and methods the needed visibility.

C programmers would use tapi.h to retrieve the constants et function declarations needed for this objective. Tanks to Project Jedi, a translation to Delphi of tapi.h is available for download from the API Conversion page of their web site as tapi.zip. This version of the header file is dated 11 September 2000 and is JEDI-certified. For convenience, the tapi.pas provided by the JEDI project has been renamed "gtrotapi.pas".

Development of the component

First, from within any project, I will do File|New|Unit and save this new unit as TAPIComponent.pas. Now that the skeleton of the component is saved, its unit can be put in any package. Compiling and installing the package will put the component GtroTAPI in the GTRO folder of the Component Palette (with a specific icon once gtroTAPIComponent.dcr is defined).

How does the component work?

Figure 1 shows how our component interacts with TAPI, the TSP (here: Unimodem /5 on Windows Vista) and the telephone hardware.

When the TgtroTAPI component is activated, the execution of the Initialize() method

  1. initializes TAPI,
  2. enumerates the devices available,
  3. selects the divice that is a modem and
  4. opens a line for monitoring.

Immediately after this is performed, the component waits for either outbound or inbound calls to occur.

What the TgtroTAPI component does

The components operates in three modes: Calling, CallerID and Answering. When the component is initialized, it is initialized in the CallerID mode, i.e., the line is opened for monitoring the line.

Initialization of the component

The public method Init() is used to initialize TAPI, to negotiate the TAPI version, and to select the modem. Its code is as follows:

function TgtroCustomTAPI.Init: Boolean;
begin
  if FTAPI_Initialized then Shutdown;
  FTAPI_Initialized:= TapiInitialize; // Initialization of TAPI
  Result:= FTAPI_Initialized;
  if Result then
  begin
    NumDevices:= EnumerateDevices; // Enumerate devices and select one (modem)
    if NumDevices > 0 then
    begin
      BearerMode:= LINEBEARERMODE_VOICE; // the only Bearer mode of interest here
      ...
      Result:= True;
    end;
  end
  else
  begin
    ShowMessage('TAPI could not initialize itself');
    Application.Terminate;
  end;
end;

As shown above, two private methods are called by Init(): TapiInitialize() and EnumerateDevices() and once they succeed, the Bearer mode is set to LINEBEARERMODE_VOICE (the only Bearer Mode of interest here). Their code follows:

function TgtroCustomTAPI.TapiInitialize: Boolean;
...
begin
  ...
  with LineInitParams do // initialize parameters
  begin
   dwTotalSize:= SizeOf(LINEINITIALIZEEXPARAMS);
   dwOptions:= LINEINITIALIZEEXOPTION_USEHIDDENWINDOW;
  end;
  ErrNo:= LineInitializeEx(@LineApp, System.MainInstance, CallbackProc,
    nil, FLocalNumDevs, FVersion, LineInitParams);
  case ErrNo of
    0: Result:= True;
    ...
  end;  // case ErrNo
  ...
end;  { TapiInitialize }

As shown in this code, the LineApp handle is returned upon successful execution of the lineInitializeEx() TAPI function. The initialization of the LineInitializeExParams structure sets the event notification mechanism to Callback (Hidden Window), the address of the callback function being given as the third argument of the function.

The private method EnumerateDevices() uses the FLocalNumDevs argument returned by lineInitializeEx() to examine the properties and capabilities of the line devices found by TAPI. As the code displayed below shows, each line device is identified and its properties are stored in Device objects. The line device that is linked to the modem is selected as the active device  (as described in the Annexes)

function TgtroCustomTAPI.EnumerateDevices: Integer;
  ...
begin
  for i:= 0 to MAXNUMDEV do fLineDevices[i].Free;
  DeviceList:= TStringList.Create;
  try
    for i := 0 to FLocalNumDevs - 1 do // FLocalNumDev is evaluated in LineInitializeEx()
    begin
      fLineDevices[i]:= TLineDevice.Create(i);
      rc:= LineNegotiateAPIVersion(LineApp, i, LoApiVer, HiApiVer, 
        fLineDevices[i].dwAPIVersion, ExtID);
      if rc <> 0 then
      begin // errors
        case rc of
          ... //Error handling
        end; // case
      end
      else
      begin // device found
        fLineDevices[i].pDevCaps:= AllocLineDevCap(LineApp, i, fLineDevices[i].dwAPIVersion, 0);
        fLineDevices[i].CallIsActive:= False;
        fLineDevices[i].rc:= rc;
        fLineDevices[i].dwBearerModes:= fLineDevices[i].pDevCaps^.dwBearerModes;
        fLineDevices[i].dwMediaModes:= fLineDevices[i].pDevCaps^.dwMediaModes;
        // Initialize line states for SetStatusMessages
        fLineDevices[i].dwLineStates:= fLineDevices[i].pDevCaps^.dwLineStates;
        if (fLineDevices[i].pDevCaps^.dwStringFormat = STRINGFORMAT_ASCII) then
        begin // Extract line name
          DeviceName:= RecordGetStr(fLineDevices[i].pDevCaps^, @fLineDevices[i].pDevCaps^.dwLineNameSize);
          DevID:= TestDevice(i);
          if DevID <> LINEMAPPER then
          begin // select the LineDevice that supports automated voice
            fModemHandle:= THandle(DevID);
            FActiveDevice:= i; // select the modem
            ...
          end; // if DevID <> LINEMAPPER

        end; // if (fLineDevices[i].pDevCaps^.dwStringFormat = STRINGFORMAT_ASCII)
        TriggerNegotiationEvent(rc, S);
      end; // if rc <> 0
    end; // for i:= 0 to FLocalNumDevs - 1
    fLineDevice:= GetDevice(FActiveDevice); // Select LineDevice of modem
    Result:= DeviceList.Count;
    TriggerEnumerateDevicesEvent(DeviceList); // produces the LineDevice list for the calling application
  finally
    DeviceList.Free;
  end; // try...finally
end;  { EnumerateDevices }

The decision ...

function TGtroCustomTAPI.TestDevice(i: Integer): DWORD;
var 
...
begin
  Result:= LINEMAPPER;
  // Open line to get valid Port Handle
  LineOpenResult := LineOpen(LineApp, i, fLineDevices[i].pHandle, 
    fLineDevices[i].dwAPIVersion, 0, 0, LINECALLPRIVILEGE_NONE, 0, nil);
  if LineOpenResult = 0  then // the line is open
  begin
    Result:= GetDeviceID(LINECALLSELECT_LINE, 'comm', i); // get valid modem handle
    LineClose(ALine);
  end
end;

where

function TgtroCustomTAPI.GetDeviceID(LineCallSelect: DWORD; 
  DeviceClass: PChar; i: Integer): DWORD;
type
  LPPort = ^TPort;
  TPort = record
    VarString: TVarString;
    DeviceID: DWORD;
    szDeviceName: array [1..1] of AnsiChar; // don't know why!!!
  end;

var 
  ...
begin
  fLineDevice:= fLineDevices[i];
  DeviceName:= 'Unknown';
  DevID:= UINT(-1);
  Result := LINEMAPPER; // so failure can be tested and defaulted

  PortSize := sizeof(TPort);
  GetMem(TempPort, PortSize); // reserve memory for TempPort
  TempPort^.VarString.dwTotalSize := PortSize;
  try
    repeat
      // lineGetIDW() returns a device identifier for the specified device class
      // associated with the selected line, address, or call.
      FError := lineGetIDW(fLineDevice.pHandle^, 0, fLineDevice.pCall^, 
        LineCallSelect, pVarString(TempPort), DeviceClass); // lpszDeviceClass: PChar
      if (TempPort^.VarString.dwNeededSize >
        TempPort^.VarString.dwTotalSize) then
      begin
        PortSize := TempPort^.VarString.dwNeededSize;
        FreeMem(TempPort);
        GetMem(TempPort, PortSize);
        TempPort^.VarString.dwTotalSize := PortSize;
        FError:= -1;
      end;
      if FError < -1 then
      begin
        TriggerDeviceIDEvent(DeviceClass, DeviceName, FError, DevID);
        Exit;
      end; // if FError
    until FError = 0;

    if (DeviceClass = 'wave/out') or (DeviceClass = 'wave/in') then ...


    if DeviceClass = 'comm' then
      if TempPort^.VarString.dwStringFormat = STRINGFORMAT_ASCII then
      begin
        if TempPort^.VarString.dwStringSize <> 0 then
          DeviceName:= RecordGetStr(TempPort^, @TempPort^.VarString.dwStringSize);
        DevID:= TempPort^.DeviceID;
        Result:= DevID;
      end;
    TriggerDeviceIDEvent(DeviceClass, DeviceName, FError, DevID);
  finally
    FreeMem(TempPort);
  end;
end

// PVarString: device identifier returned here

Opening the line

Up to now, TAPI has been initialized and the modem has been selected. This process has been executed without having to specify any purpose for the use of the component. With the opening of the line, this changes. Some parameters having to do with the intent one has to open the line have to be specified

  1. the Bearer Mode which in this case must be voice (regular 3.1 kHz analog voice-grade bearer service) since the modem supports only this mode and the passtrough (service provider gives direct access to the attached hardware for control by the application) mode;
  2. the Media Mode which in this case will either be interactivevoice (call is treated as an interactive call with humans on both ends) and automatedvoice (voice is locally handled by an automated application); and
  3. the CallPriviledges which can take the set of values of LINECALLPRIVILEDGE_NONE, LINECALLPRIVILEDGES_MONITOR or LINECALLPRIVILEDGE_OWNER, the meanings of which seems obvious.

As shown below, the selection of these modes is performed through the definition of the TAPIMode property that can take the following values:

The code of the OpenLine() method is the following:

function TgtroCustomTAPI.OpenLine(CallPriviledge: TTAPICallPriviledges): Boolean;
var 
  ...
begin
  Result:= False;
  with FLineCallParams do // a TLineCallParams structure
    begin
      dwTotalSize := sizeof(FLineCallParams);
      dwBearerMode := BearerMode;
      dwMediaMode := MediaMode;
    end;
  case CallPriviledge of
    cpNone: CallPriviledges:= LINECALLPRIVILEGE_NONE; // placing calls
    cpOwner: CallPriviledges:= LINECALLPRIVILEGE_OWNER; // answering calls
    cpMonitor: CallPriviledges:=
      LINECALLPRIVILEGE_MONITOR or LINECALLPRIVILEGE_OWNER; // monitoring calls
  end; // case

  // Open the line now!
  OpenResult := LineOpen(LineApp, FActiveDevice, fLineDevice.pHandle, fLineDevice.dwAPIVersion, 0, 0,  // No data passed back
    fLineDevice.dwCallPriviledges, fLineDevice.dwMediaMode, nil);

  if OpenResult = 0 then
    begin
      fLineDevice.LineIsOpen:= True;
      LineSetStatusMessages(fLineDevice.pHandle^, fLineDevice.dwLineStates, 0);// Enables LINE_LINEDEVSTATE messages

      Result := True; // success so drop through

    end
    else
    begin
      case OpenResult of
         ...
      end;
    if OpenResult < 0 then
      raise Exception.Create(OpenMsg);
    end;
  TriggerLineOpenEvent(OpenMsg);
end;  { OpenLine }

Now that the line object is open, lets consider how TAPI communicates with the application.

Callback function

As a result of the selection of "Callback" as the notification mechanism when lineInitializeEx() was executed, I will use a callback function. In the context of object-oriented programming, this function must be global and cannot be a method of the class. As a consequence, I will use a global procedure as our callback function and use a pointer to our class in order for the callback procedure to communicate with our TGtroCustomTapi object. This pointer, called MyGtroTAPIComponent,  is declared as a variable of the type of the class and it is initialized to "self" in the OnCreate() method of the class.

The callback procedure is implemented as follow:

procedure CallBackProc(hDevice, dwMessage, dwInstance, dwParam1, dwParam2, dwParam3 : DWORD); stdcall;
begin
MyGtroTAPIComponent.TapiCallBack(hDevice, dwMessage, dwInstance, dwParam1, dwParam2, dwParam3);
end;

where TapiCallBack() is a private method (a hack) of the TCustomGtroTapi class with the same arguments as CallbackProc().  This private methods is coded as follows:

procedure TgtroCustomTAPI.TapiCallback
  (hDevice, dwMessage, dwInstance, dwParam1, dwParam2, dwParam3: DWORD);
begin
  if FTAPI_Initialized then
  begin
    with CallbackResults do
    begin // Write TAPI results to string list for use by Delphi applications
      Clear;

      case dwMessage of // hDevice is HLINE
        LINE_ADDRESSSTATE,
        LINE_APPNEWCALL, // dwParam2 is HCALL
        LINE_CLOSE,
        LINE_LINEDEVSTATE:
        LineProc(hDevice, dwMessage, dwInstance, dwParam1, dwParam2, dwParam3);

        // hDevice is HCALL
        LINE_CALLINFO,
        LINE_CALLSTATE,
        LINE_GATHERDIGITS,
        LINE_GENERATE,
        LINE_MONITORDIGITS,
        LINE_MONITORMEDIA:
        CallProc(hDevice, dwMessage, dwInstance, dwParam1, dwParam2, dwParam3);

        LINE_CREATE:
        begin
          // hDevice is zéro, dwParam1 identifies the device
          Add(DDT + 'received LCB (LINE_CREATE): Line device created')
        end; // LINE_CREATE

        LINE_REMOVE:
        begin
          // hDevice is zero; dwParam1 identifies the device
          Add(DDT + 'received LCB (LINE_REMOVE): Line device removed');
        end; // LINE_REMOVE

        LINE_REPLY:
        begin
          if (dwParam2 = 0) then
            Add(DDT + 'received LCB (LINE_REPLY): Async. request 0x:' +
          IntToHex(dwParam1, 8) + ' completed successfully')
        else
          Add(DDT + 'received LCB (LINE_REPLY): Async. request 0x:' +
          IntToHex(dwParam1, 8) + ' failed');
          TriggerLineReplyEvent;
        end; // LINE_REPLY

        LINE_REQUEST:
        begin
          // hDevice is zero; dwParam1 is the request mode
          Add(DDT + 'received LCB (LINE_REQUEST): Request from another application');
        end; // LINE_REQUEST

        else
          Add(DDT + 'received LCB (UNKNOWN): Unpredicted event 0x' + IntToHex(dwMessage, 8));
        end; // case dwMessage

        if Verbose then
        begin
          Add('Parameters: : ' + IntToHex(hDevice, 8) + ', ' + IntToHex(dwMessage, 8)
            + ', ' + IntToHex(dwInstance, 8) + ', ' + IntToHex(dwParam1, 8)
            + ', ' + IntToHex(dwParam2, 8) + ', ' + IntToHex(dwParam3, 8));
        end;
      end;  // CallbackResults
    TriggerCallbackEvent; // sends results to the calling application
  end; // if FTAPI_Initialized
end;

Porting the component to Delphi 2009

The component has been used successfully with Delphi 6.0 and Delphi 7.0 but it cannot be used under Delphi 2009 because it does not recognize the modem. In order to do so, the component uses two methods that use strings and Char types: RecordGetString() and GetDeviceID() which in turn uses the lineGetID() TAPI call.

Where previous versions of Delphi used a String type based on ANSI Character types of only 1 byte long, Delphi 2009 defines a new string type based on Unicode data, with WideChar elements of 2 bytes long. Delphi 2009 is fully Unicode based, and defines a new type called UnicodeString which is the new equivalent for the String type. Previously, String was synonymous with AnsiString (a type which is also still available, just like AnsiChar and PAnsiChar). ASdditionally, Delphi 2009 Character types are Char, AnsiChar and WideChar, where Char defaults to WideChar. In previous versions of Delphi, a Char would be equivalent to an AnsiChar. In order to ensure existing code compiler without changes in behaviour, change Char to AnsiChar (as well as PChar to PAnsiChar). The most important Delphi 2009 String types are: UnicodeString, WideString, AnsiString, UTF8String (a AnsiString with UTF-8 encoding) and ShortString. The default String type is equivalent to UnicodeString, which consists of WideChar characters (like WideString), but is reference counted and memory managed by Delphi (instead of by Windows itself), so a lot faster than a WideString. [extracted from Delphi 2009 Unicode]

Tapi 2.0 and Unicode

According to MSDN, most TAPI functions are implemented in Unicode (W) and ANSI (A) versions whereas the entire Telephony Service Provider Interface (TSPI) is Unicode for version 2.0. Applications that explicitly call the generic (neither "W" or "A" suffix) version of a function will execute the ANSI version, for compatibility with previous versions of TAPI.

In the component, the only TAPI calls that are used are (listed alphabetically):

Two instances of TAPI calls deserve a special attention.

GetDeviceID()

In the process of enumerating the TAPI devices available, each device is scrutinized to determine if it is a modem or not. This is done using the GetDeviceID() method whose code is given in part hereunder:

function TgtroCustomTAPI.GetDeviceID(LineCallSelect: DWORD; DeviceClass: PChar;
  i: Integer): DWORD;
// Return the LineDevice ID for "comm", "wave/in" and "wave/out".
type
  LPPort = ^TPort;
  TPort = record
    VarString: TVarString;
    // good trick as this becomes the additional param provided by TAPI
    DeviceID: DWORD;
    szDeviceName: array [1..1] of AnsiChar; // don't know why!!!
  end;

var
  ...
begin
  fLineDevice:= fLineDevices[i];
  DeviceName:= 'Unknown';
  DevID:= UINT(-1);
  Result := LINEMAPPER; // so failure can be tested and defaulted
  //if not LineIsOpen then Exit; // no line open => bail out
  PortSize := sizeof(TPort);
  GetMem(TempPort, PortSize); // reserve memory for TempPort
  TempPort^.VarString.dwTotalSize := PortSize;
  try
    repeat
      // lineGetIDW() returns a device identifier for the specified device class
      // associated with the selected line, address, or call.
      FError := lineGetIDW( // unicode version of the generic lineGetID() API call
        fLineDevice.pHandle^, // current value of the line handle
        0,
        fLineDevice.pCall^,  // call exist
        LineCallSelect,
        pVarString(TempPort),// PVarString: DeviceID returned here
        DeviceClass); // lpszDeviceClass: PChar
      if (TempPort^.VarString.dwNeededSize >
        TempPort^.VarString.dwTotalSize) then
      begin
        PortSize := TempPort^.VarString.dwNeededSize;
        FreeMem(TempPort);
        GetMem(TempPort, PortSize);
        TempPort^.VarString.dwTotalSize := PortSize;
        FError:= -1;
      end;
      if FError < -1 then
      begin
        TriggerDeviceIDEvent(DeviceClass, DeviceName, FError, DevID);
        Exit;
      end; // if FError
    until FError = 0;

    if (DeviceClass = 'wave/out') or (DeviceClass = 'wave/in') then
      if TempPort^.VarString.dwStringFormat = STRINGFORMAT_BINARY then
      begin
        if TempPort^.VarString.dwStringSize <> 0 then
          DeviceName:= RecordGetStr(TempPort^, @TempPort^.VarString.dwStringSize);
        DevId:= TempPort^.DeviceID;
        Result:= DevID;
      end;

    if DeviceClass = 'comm' then
      if TempPort^.VarString.dwStringFormat = STRINGFORMAT_ASCII then
      begin
        if TempPort^.VarString.dwStringSize <> 0 then
          DeviceName:= RecordGetStr(TempPort^, @TempPort^.VarString.dwStringSize);
        DevID:= TempPort^.DeviceID;
        Result:= DevID;
      end;
    TriggerDeviceIDEvent(DeviceClass, DeviceName, FError, DevID);
  finally
    FreeMem(TempPort);
  end;
end;

After porting from Delphi 7.0, this part of the method returned the constant LINERR_INVALDEVCLASS with the result that the recognition of the modem failed. It took some time before finding that with TAPI 2.0, applications that explicitly call the generic (neither "W" or "A" suffix) version of a this function will execute the ANSI version, for compatibility with previous versions of TAPI. The solution was to use lineGetIDW() instead of lineGetID().

RecordGetString()

This is first method that I had to modify in order to execute the component correctly. It manipulates character data from the LINEDEVCAPS structure. Its code is as follows:

with is called as follows:

DeviceName:= RecordGetStr(fLineDevices[i].pDevCaps^, @fLineDevices[i].pDevCaps^.dwLineNameSize);

where

First, the method processes the Field argument. Field is a pointer (an address) on the dwLineNameSize member of the pDevCaps member of the TLineDeviceRecord so that dereferencing it result in an Integer type assigned to the local variable Len.

In the next statement, PInt(Field) is increased so that it points to the integer next to Field which is the dwLineOffset member of the device capability record.

Note that

This having been said, then, what does the statement

if (PInt(Field)^ <> 0) then
  SetString(Result, PAnsiChar(Longint(@Data) + PInt(Field)^), Len - 1) // Delphi 2009

do? If the offset is different from zero, the content of this variably sized field containing the name of the line device is transformed into an PAnsiChar and is transfered into the Result of the method.

Conclusion

All this work was performed for a project called "The MailBox project" that aimed at developing an in-process server that would provide dialing, CallerID and answering machine capabilities to another of my applications. This project was abandonned later since I could not obtain reliable operation with my modem.

You can download the code from here.

Annexes

References

  1. "Delphi and TAPI Part III: Wrapping Up Telephony by Allen Moore and Ken Kyler, Delphi Informant, September 1998 (no more references to this article on the Web).
  2. "The Tomes of Delphi -  Basic 32-bit Communications Programming" by Allen C. Moore and  John C. Penman

Variable structures

Variable structures such as TLINEDEVCAPS (used in EnumerateDevices()) and TLINECALLINFO (used in LineCallBack() to handle the events LINECALLINFOSTATE_CALLERID and LINE_APPNEWCALL) can be very tricky in TAPI, since they often vary in size from one version to the next. It is quite possible to pass a size that is too small in the dwTotalSize parameter. When this happens, an error occurs and the situation can be handled by reallocating memory based on the dwNeededSize parameter. 

Here follows an example using the TLINEDEVCAPS structure of how I have tackled such situation.The structure t is declared as follows:

TLineDevCaps = packed record
    dwTotalSize,  // The total size in bytes allocated to this data structure
    dwNeededSize, // The size(bytes) for this structure that is needed to hold all the returned information.
    dwUsedSize,   // The size in bytes of the portion of this data structure that contains useful information.
... // all other fields removed
end;

and the memory allocation proceeds as follows.

function TgtroCustomTAPI.AllocLineDevCaps(hLineApp: HLINEAPP; dwDeviceID,
  dwAPIVersion, dwExtVersion: DWORD): PLINEDEVCAPS;
// Called by EnumerateDevices
var
  AllocSize: Integer;
  rc: longint;
begin
  AllocSize:= SizeOf(TLINEDEVCAPS);
  Result:= AllocMem(AllocSize); //  will be filled with zeros
  Result^.dwTotalSize:= AllocSize;
  rc:= lineGetDevCaps(hLineApp, dwDeviceID,
    dwAPIVersion, dwExtVersion, Result); // get call info
  if Result^.dwNeededSize > Result^.dwTotalSize then
  begin
    AllocSize:= Result^.dwNeededSize;
    ReallocMem(Result, AllocSize);
      Result^.dwTotalSize:= AllocSize;
    rc:= lineGetDevCaps(hLineApp, dwDeviceID, dwAPIVersion, dwExtVersion, Result);
  end;
  TapiCheck(rc); // raise exception if TAPI failed
end;

As one can see, a first memory allocation is tried using the size of the TLINEDEVCAPS structure and assigning this size to the dwTotalSize member of the structure. After this memory allocation, the dwNeededSize member is read and compared to the size of the first allocation. Given that more memory is needed, the memory is reallocated with the value of the dwNeededSize. Finally, the value of the return value of the functionlineGetDevCaps() is checked for errors.

Selection of the modem

The EnumerateDevices() method does not only enumerate the line devices found by lineInitializeEx() and exhibits their capabilities, is selects the line device associated with the com port using the private method TestDevice().

function TGtroCustomTAPI.TestDevice(i: Integer): DWORD;
var
  LineOpenResult : longint;
  ALine: HLINE;
begin
Result:= LINEMAPPER;
 // Open line to get valid Port Handle : June 27, 1998
  LineOpenResult := LineOpen(LineApp, i, FDevices[i].Handle,
    FDevices[i].dwAPIVersion, 0, 0, LINECALLPRIVILEGE_NONE, 0, nil);
  if LineOpenResult = 0  then // the line is open
  begin
    // get valid modem handle
    FDevices[i].LineIsOpen:= True;
    Result:= GetDeviceID(LINECALLSELECT_LINE, 'comm', i);
    LineClose(ALine);
    FDevices[i].LineIsOpen:= False;
  end
  end;

This method tries to open a line and quits if it fails. If it succeeds, it calls the method GetDeviceID():

function TgtroCustomTAPI.GetDeviceID(LineCallSelect: DWORD; DeviceClass: PChar;
  i: Integer): DWORD;

// to return the device ID for "comm", "wave/in" and "wave/out".
type
  LPPort = ^TPort;
  TPort = record
    VarString: TVarString;
    // good trick as this becomes the additional param provided by TAPI
    DeviceID: DWORD;
    szDeviceName: array [1..1] of Char; // don't know why!!!
  end;

var
  TempPort: LPPort; PortSize: LongInt; FError: longint;
  DeviceName: string; DevID: DWORD; FDevice: TDevice;
begin
  FDevice:= FDevices[i];
  DeviceName:= 'Unknown';
  DevID:= UINT(-1);
  Result := LINEMAPPER; // so failure can be tested and defaulted
  if not FDevices[i].LineIsOpen then Exit; // no line open => bail out
  PortSize := sizeof(TPort);
  GetMem(TempPort, PortSize);
  TempPort^.VarString.dwTotalSize := PortSize;
  try
    repeat
      FError := lineGetID(
        FDevice.Handle^, // current value of the line handle
        0,               // ignored
        FDevice.Call^,   // ignored
        LineCallSelect,
        pVarString(TempPort), // pointer to a memory location of type VARSTRING, where the device ID is returned.  
        DeviceClass); 
      if FError < 0 then
      begin
        TriggerDeviceIDEvent(DeviceClass, DeviceName, FError, DevID); // added by me
        Exit;
      end; // if FError
      if (TempPort^.VarString.dwNeededSize >
        TempPort^.VarString.dwTotalSize) then
      begin
        PortSize := TempPort^.VarString.dwNeededSize;
        FreeMem(TempPort);
        GetMem(TempPort, PortSize);
        TempPort^.VarString.dwTotalSize := PortSize;
        FError:= -1;
      end;
    until FError = 0;

    if (DeviceClass = 'wave/out') or (DeviceClass = 'wave/in') then
      if TempPort^.VarString.dwStringFormat = STRINGFORMAT_BINARY then
      begin
        if TempPort^.VarString.dwStringSize <> 0 then
          DeviceName:= RecordGetStr(TempPort^, @TempPort^.VarString.dwStringSize);
        DevId:= TempPort^.DeviceID;
        Result:= DevID;
      end;

    if DeviceClass = 'comm' then
      if TempPort^.VarString.dwStringFormat = STRINGFORMAT_ASCII then
      begin
        if TempPort^.VarString.dwStringSize <> 0 then
          DeviceName:= RecordGetStr(TempPort^, @TempPort^.VarString.dwStringSize);
        DevID:= TempPort^.DeviceID;
        Result:= DevID;
      end;
    TriggerDeviceIDEvent(DeviceClass, DeviceName, FError, DevID); // added by me
  finally
    FreeMem(TempPort);
  end;
end;

The code of this function has been provided to Allen Moore and Ken Kyler (Reference 1) by  Keith Anderson, Keith@PureScience.com and I have modified it to fit the purpose of this project. It opens a line for each line device (referred to by the argument i) et, in this case, it uses the 'comm' device class. Once the line is open, it uses the API function LineGetID() to get a device ID for the specified device class associated with the selected line (I used LINECALLSELECTLINE in the call to the function).

Upon successful completion of the function call, the pVarString(TempPort) argument is filled with the name of the device and its device ID. The format of the returned information depends on the method used by the device class API for naming devices. Prior to calling lineGetID(), I have set the dwTotalSize field of this structure to indicate the amount of memory available to TAPI for returning information. Since the structure has variable size, room has been left for an investigation of the dwNeededSize field and a retry of lineGetID() once the right size of the structure has been determined.

Obviously, the method fails when there is no true line device attached to the line (the DevID is then set to LINEMAPPER). When it succeeds, it returns a DWORD that can be cast to the modem handle as THandle(DevID). Note that this method can also be used for other device classes as it does for 'wave/in" and 'wave/out' in other circumstances in the component.

The code of the RecordGetStr() method used to extract the the DeviceName reads as follows:

function RecordGetStr(var Data; Field : Pointer) : string;
var
  Len: Longint;
begin
  Len := PInt(Field)^; // PInt is defined in Windows.pas
  Inc(PInt(Field));
  if (PInt(Field)^ <> 0) then
     SetString(Result, PChar(Longint(@Data) + PInt(Field)^), Len - 1)
  else
     Result:='';
end;

Warning!
This code was developed for the pleasure of it. Anyone who decides to use it does so at its own risk and agrees not to hold the author responsible for its failure.


Questions or comments?
E-Mail
Last modified: September 4th 2014 16:22:17. []