Using OPC from C/C++

Help Contents

Introduction

You can create programs in C/C++ to access data in Fernhill SCADA. This article shows how to use the OPC Server to query an analog tag value.

The structure of this example program is:

  1. Use CoInitialize() to initialize COM
  2. Use CLSIDFromProgID() to translate a human readable ProgID to a CLSID
  3. Use CoCreateInstance() to create an instance of the OPC Server
  4. Use IOPCServer::GetStatus() to query the status of the OPC Server
  5. Use IOPCServer::QueryInterface() to query for the IOPCItemIO interface
  6. Use IOPCItemIO::Read() to read tag values

Header File References

Typically these header file references are required to reference the appropriate windows libraries and OPC interface definitions:

// Windows header files
#include <SDKDDKVer.h>
#include <Windows.h>

// C Header files
#include <stdio.h>
#include <tchar.h>

// OPC Header files
#include "opcda.h"
#include "opcda_i.c"

Main Program

The main program initializes COM, then calls a set of helper functions to create an OPC Server and call methods on that server:

int wmain( int argc, wchar_t* argv[] )
{
    // Initialize COM.
    HRESULT Hr = ::CoInitialize( 0 );
    if ( FAILED( Hr ) )
    {
        // Report the failure
        ReportHRESULTError( Hr, 0, L"CoInitialize()" );
    }
    else
    {
        // Create an instance of IOPCServer...
        IOPCServer *pServer = CreateOPCServerInstance( L"FernhillSoftware.FernhillSCADA" );
        if ( pServer )
        {
            // Query the status of the OPC Server
            QueryOPCServerStatus( pServer );

            // Read a tag value
            ReadTagValue( pServer, L"localhost.PumpStation.TotalCurrent" );

            // Release the instance of the OPC Server
            pServer->Release();
        }

        // Finished with COM
        ::CoUninitialize();
    }
    return 0;
}

Creating an Instance of an OPC Server

The function CreateOPCServerInstance uses CoCreateInstance() passing an appropriate CLSID to create an instance of an OPC Server:

IOPCServer *CreateOPCServerInstance( const wchar_t *ProgId )
{
    wprintf( L"Creating OPC Server instance from '%s'...\n", ProgId );

    // Translate the human readable OPC Server ProgID to the less readable CLSID
    CLSID ClassId;
    HRESULT Hr = ::CLSIDFromProgID( ProgId, &ClassId );
    if ( FAILED( Hr ) )
    {
        ReportHRESULTError( Hr, 0, L"CLSIDFromProgID( '%s' )", ProgId );
        return 0;
    }

    // Attempt to create the instance
    IOPCServer *pOPCServer;
    Hr = ::CoCreateInstance(
            ClassId,
            0,
            CLSCTX_ALL,
            IID_IOPCServer,
            reinterpret_cast<void **>( &pOPCServer ) );
    // Handle any error
    if ( FAILED( Hr ) )
    {
        ReportHRESULTError( Hr, 0, L"CoCreateInstance()" );
        return 0;
    }

    wprintf( L"...succeeded\n" );
    return pOPCServer;
}

Querying OPC Server Status

The function QueryOPCServerStatus uses IOPCServer::GetStatus() method to query information from the connected OPC Server:

void QueryOPCServerStatus( IOPCServer *pOPCServer )
{
    wprintf( L"Query OPC Server status...\n" );

    // Query the server status
    OPCSERVERSTATUS *pServerStatus;
    HRESULT Hr = pOPCServer->GetStatus( &pServerStatus );
    if ( FAILED( Hr ) )
    {
        // Report the error
        ReportHRESULTError( Hr, pOPCServer, L"GetStatus()" );
    }
    else
    {
        // Print the server status
        wprintf( L"\tVendor:  %s\n", pServerStatus->szVendorInfo );
        wprintf( L"\tVersion: %hu.%hu.%hu\n", 
                    pServerStatus->wMajorVersion, 
                    pServerStatus->wMinorVersion, 
                    pServerStatus->wBuildNumber );
        wprintf( L"\tStatus:  %lu\n", pServerStatus->dwServerState );

        // Recover memory from output parameters as per COM rules
        ::CoTaskMemFree( pServerStatus->szVendorInfo );
        ::CoTaskMemFree( pServerStatus );
    }
}

Querying a Tag Value

The function ReadTagValue queries for the IOPCItemIO interface and then calls IOPCItemIO::Read() to read tag values:

void ReadTagValue( IOPCServer *pServer, const wchar_t *TagName )
{
    wprintf( L"Read tag '%s'...\n", TagName );

    // The most direct way of reading a tag value is to use the IOPCItemIO interface.
    // However this interface is only available from Version 3.0 servers.
    // If you have an older server, the sequence of calls would be:
    //	Use IOPCServer::AddGroup() to add a new group
    //	Use IOPCItemMgt::AddItems() to add the tag to the group
    //	Use IOPCSyncIO::Read() to read the values of the items in the group
    IOPCItemIO *pOPCItemIO;
    HRESULT Hr = pServer->QueryInterface( IID_IOPCItemIO, reinterpret_cast<void **>( &pOPCItemIO ) );
    if ( FAILED( Hr ) )
    {
        ReportHRESULTError( Hr, pServer, L"QueryInterface( IOPCItemIO )" );
    }
    else
    {
        DWORD dwCount = 1;
        LPCWSTR pszItemIDs[ 1 ] = { TagName };
        DWORD dwMaxAge[ 1 ] = { 0 };
        VARIANT *pValues;
        WORD *pQualities;
        FILETIME *pTimestamps;
        HRESULT *pErrors;
        Hr = pOPCItemIO->Read(
                dwCount,
                pszItemIDs,
                dwMaxAge,
                &pValues,
                &pQualities,
                &pTimestamps,
                &pErrors );
        if ( FAILED( Hr ) )
        {
            ReportHRESULTError( Hr, pServer, L"IOPCItemIO::Read()" );
        }
        else
        {
            wprintf( L"\n" );

            for ( DWORD Index = 0; Index < dwCount; ++ Index )
            {
                // Check the item result
                HRESULT ItemError = pErrors[ Index ];

                if ( FAILED( ItemError ) )
                {
                    // Error in item
                    ReportHRESULTError( ItemError, pServer, L"Item[%lu] Error", Index );
                }
                else
                {
                    // Successfully read the tag.  Get the value
                    VARIANT *pValue = &pValues[ Index ];

                    // For convenient convert to a string...
                    ItemError = ::VariantChangeTypeEx(
                            pValue,
                            pValue,
                            LOCALE_SYSTEM_DEFAULT,
                            0,
                            VT_BSTR );
                    if ( FAILED( ItemError ) )
                    {
                        ReportHRESULTError( ItemError, pServer, L"Item[%lu] ChangeType", Index );
                    }
                    else
                    {
                        wprintf( L"%s = %s\n", pszItemIDs[ Index ], pValue->bstrVal );
                    }
                }
            }

            wprintf( L"\n" );

            // Recover memory
            for ( DWORD Index = 0; Index < dwCount; ++ Index )
                ::VariantClear( &pValues[ Index ] );
            ::CoTaskMemFree( pValues );
            ::CoTaskMemFree( pQualities );
            ::CoTaskMemFree( pTimestamps );
            ::CoTaskMemFree( pErrors );
        }

        // Finished with the IOPCItemIO interface
        pOPCItemIO->Release();
    }
}

Error Reporting

OPC interface methods can return standard windows errors, for example E_FAIL, or OPC specific errors, for example OPC_E_INVALIDHANDLE. It is useful to have a single error reporting function that can handle either error:

void ReportHRESULTError( HRESULT Result, IOPCServer *pOPCServer, const wchar_t *Function, ... )
{
    va_list Args;
    va_start( Args, Function );

    // Print the function call and the raw result value
    vwprintf( Function, Args );
    wprintf( L" returns 0x%08lX:\n", Result );

    // Try and convert "Result" to text...
    // Try Windows first...
    wchar_t Buffer[ 256 ];
    DWORD BufferLength = 256;
    DWORD Length = 
        ::FormatMessage(
            FORMAT_MESSAGE_FROM_SYSTEM,
            0,
            Result,
            0,
            Buffer,
            BufferLength,
            0 );
    if ( Length != 0 )
    {
        // A Windows error.  Print it
        wprintf( L"%s", Buffer );
    }
    else if ( pOPCServer )
    {
        // "Result" might be an OPC error.
        // If the OPC server is available, try and use the server to translate the error
        LPWSTR pErrorString;
        HRESULT Hr = 
            pOPCServer->GetErrorString( 
                Result, 
                LOCALE_SYSTEM_DEFAULT, 
                &pErrorString );
        if ( SUCCEEDED( Hr ) )
        {
            // Error message available
            wprintf( L"%s\n", pErrorString );

            // Recover memory as per COM rules
            ::CoTaskMemFree( pErrorString );
        }
    }
    va_end( Args );
}

Further Information

OPC Server

To learn about OPC.

OPC Server Administration Tool

To learn how to connect OPC Clients to different computers running Fernhill SCADA.

Glossary

For the meaning of terms used in Fernhill SCADA.