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:
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"
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; }
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; }
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 ); } }
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(); } }
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 ); }
To learn about OPC.
OPC Server Administration ToolTo learn how to connect OPC Clients to different computers running Fernhill SCADA.
For the meaning of terms used in Fernhill SCADA.