基於第三方開源庫的OPC服務器開發指南(4)——后記:與另一個開源庫opc workshop庫相關的問題


平心而論,我們從樣例服務器的代碼可以看出,利用LightOPC庫開發OPC服務器還是比較啰嗦的,網上有人提出opc workshop庫就簡單很多,我千辛萬苦終於找到一個05年版本的workshop庫源碼,忘了出處是在哪里了,依稀記得是Codeforge網站。相較於LightOPC,用這個庫開發OPC服務器確實簡單了很多,其對核心業務邏輯做了高度封裝,使得服務器的開發流程非常清晰,這一點值得贊揚。但遺憾的是,完美的事情在這個世界上根本就不存在,經過實測,我手頭上擁有的版本存在三個嚴重問題:

1、利用該庫開發的OPC服務器無法由OPC客戶端遠程啟動;

2、通過標准接口ValidateItems()無法獲取指定變量的數據類型;

3、提供的樣例服務器主處理邏輯存在重復注冊的BUG,沒有把服務器注冊和處理邏輯分開;

 

好在已經有了LightOPC這碗酒墊底,這幾個問題都不是問題。我的方法簡單粗暴——直接上手改源碼。對於第一個問題,通過分析源碼發現,導致該問題的原因是注冊函數在獲取模塊文件工作路徑時,接收緩沖區的首地址錯誤導致的:

  1 int COPCServerObject::RegisterServer()
  2 {
  3     char np[FILENAME_MAX + 32];
  4     printf("Registering");
  5     GetModuleFileName(NULL, np + 1, sizeof(np) - 8);
  6 
  7     return ServerRegister(&CLSID_OPCServerEXE,
  8                                OPCServerProgID,
  9                                "OPCServer (c) Alexey Obukhov", np, 0);
 10 }

出問題的這個注冊函數在OPCServerObject.cpp文件中,不知道是什么原因讓作者在獲取進程工作路徑時將緩沖區首地址后移了一個字節,即:

  1 GetModuleFileName(NULL, np + 1, sizeof(np) - 8);

至今我沒參透為何要“np + 1”。事實證明,把后面加的那個“1”去掉后,服后務器不僅可以遠程啟動了且工作也完全正常。看來這件事需要作者本人親自解釋這到底是為什么了,咱們只要能用就行了。

第2個問題更加匪夷所思,作者提供的“ValidateItems()”接口函數竟然缺少了關鍵的對變量類型的賦值語句:

  1     STDMETHOD(ValidateItems)( /*[in]*/ DWORD dwCount,
  2         /*[in, size_is(dwCount)]*/ OPCITEMDEF * pItemArray,
  3         /*[in]*/ BOOL bBlobUpdate,
  4         /*[out, size_is(,dwCount)]*/ OPCITEMRESULT ** ppValidationResults,
  5         /*[out, size_is(,dwCount)]*/ HRESULT ** ppErrors )
  6     {
  7         DWORD i;
  8         HRESULT res = S_OK;
  9         OPC_GROUP_CHECK_DELETED();
 10 
 11         VALIDATE_ARGUMENT(pItemArray);
 12         VALIDATE_ARGUMENT(ppValidationResults);
 13         VALIDATE_ARGUMENT(ppErrors);
 14 
 15         *ppValidationResults = allocate_buffer<OPCITEMRESULT> ( dwCount );
 16         *ppErrors = allocate_buffer<HRESULT> ( dwCount );
 17 
 18         // TODO
 19         for( i=0;i<dwCount; ++i) {
 20             OPCHANDLE hServer = g_NameIndex[ CString(pItemArray[i].szItemID) ];
 21             CBrowseItemsList::iterator browseIT = g_BrowseItems.find( hServer );
 22             if( browseIT == g_BrowseItems.end() ) {
 23                 (*ppErrors)[i] = OPC_E_UNKNOWNITEMID;
 24                 res = S_FALSE;
 25             }
 26         }
 27         // TODO
 28 
 29         return res;
 30     }

上述函數在IOPCItemMgtImpl.h源文件中可以找到。其中入口參數“ppValidationResults”即被用於獲取指定變量的相關信息。但奇怪的是,在這個函數里作者只是對這個變量分配了一塊內存,接下來的代碼並沒有對其賦值。如果說我到手的源碼並不完整的話,那么為何解決上述幾個問題后,OPC服務器竟然工作正常,沒有任何問題?要不說這個問題很是匪夷所思呢。既然咱們有源碼,這個事完全可以自己解決,在這個函數增加幾行代碼:

  1     STDMETHOD(ValidateItems)( /*[in]*/ DWORD dwCount,
  2         /*[in, size_is(dwCount)]*/ OPCITEMDEF * pItemArray,
  3         /*[in]*/ BOOL bBlobUpdate,
  4         /*[out, size_is(,dwCount)]*/ OPCITEMRESULT ** ppValidationResults,
  5         /*[out, size_is(,dwCount)]*/ HRESULT ** ppErrors )
  6     {
  7         DWORD i;
  8         HRESULT res = S_OK;
  9         OPC_GROUP_CHECK_DELETED();
 10 
 11         VALIDATE_ARGUMENT(pItemArray);
 12         VALIDATE_ARGUMENT(ppValidationResults);
 13         VALIDATE_ARGUMENT(ppErrors);
 14 
 15         *ppValidationResults = allocate_buffer<OPCITEMRESULT> ( dwCount );
 16         *ppErrors = allocate_buffer<HRESULT> ( dwCount );
 17 
 18         /// TODO
 19         for( i=0;i<dwCount; ++i) {
 20             OPCHANDLE hServer = g_NameIndex[ CString(pItemArray[i].szItemID) ];
 21             CBrowseItemsList::iterator browseIT = g_BrowseItems.find( hServer );
 22             if( browseIT == g_BrowseItems.end() ) {
 23                 (*ppErrors)[i] = OPC_E_UNKNOWNITEMID;
 24                 res = S_FALSE;
 25             }
 26             else
 27             {
 28                 (*ppValidationResults)->vtCanonicalDataType = browseIT->type;
 29                 break;
 30             }
 31         }
 32         // TODO         
 33 
 34         return res;
 35     }

連花括號都算着其實就增加了4行代碼。只是對參數“ppValidationResults”的數據類型成員“vtCanonicalDataType”進行了賦值。如此一來,“ValidateItems()”接口即可滿足我們的要求了。

第3個問題就簡單多了,直接修改樣例服務器的“main()”函數把注冊和主處理邏輯分開就可以了:

  1 int _tmain(int argc, _TCHAR* argv[])
  2 {
  3     FILE *pfFile;
  4 
  5     AllocConsole();
  6     freopen_s(&pfFile,"conout$","w+",stdout);    //打䨰開a控?制?台¬¡§   
  7 
  8     if(argc > 2)
  9     {
 10         printf("Usage:%s", argv[0]);
 11         printf("      %s /r", argv[0]);
 12         printf("      %s /u", argv[0]);
 13         printf("  : start opc server\r\n");
 14         printf("/r: regist opc server\r\n");
 15         printf("/u: unregist opc server\r\n");
 16 
 17         fclose(pfFile);
 18         FreeConsole();
 19 
 20         return -1;
 21     }
 22 
 23     char str[1024] = {0};
 24 
 25     HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
 26 
 27     // define server object 
 28     COPCServerObject server;
 29     // define data event receiver 
 30     dataReceiver receiver;
 31 
 32     // set server name and clsid
 33     server.setServerProgID( _T("OPC.myTestServer") );
 34     server.setServerCLSID( CLSID_OPCServerEXE );
 35 
 36     // set delimeter for params name 
 37     server.SetDelimeter( "." );
 38 
 39     if(argc == 2)
 40     {
 41         if(strstr(argv[1], "/r"))
 42         {
 43             // register server as COM/DCOM object 
 44             server.RegisterServer();
 45 
 46             fclose(pfFile);
 47             FreeConsole();
 48 
 49             return 0;
 50         }
 51         else if(strstr(argv[1], "/u"))
 52         {
 53             server.UnregisterServer();
 54 
 55             getchar();
 56 
 57             fclose(pfFile);
 58             FreeConsole();
 59 
 60             return 0;
 61         }
 62     }
 63 
 64 
 65     // define server values tree
 66     server.AddTag("Values.int1", VT_I4 );
 67     server.AddTag("Values.int2", VT_I4 );
 68     server.AddTag("Values.fltArray2", VT_ARRAY|VT_R4 );
 69     server.AddTag("Values.fltArray2.In", VT_I4, false );
 70 
 71     {
 72         CAG_Clocker cl("Create 10000 tags",false);
 73 
 74         for(int i=0;i<10000;++i) {
 75             sprintf(str,"RandomValues.int%d",i+1);
 76             server.AddTag( str ,VT_I4 );
 77         }
 78     }
 79 
 80     // setup object will be received add values change 
 81     server.setDataReceiver( &receiver );
 82 
 83     // create COM class factory and register it 
 84     server.StartServer();
 85 
 86     printf("\t waiting return\n");
 87     gets(str);  // 等待用戶任意輸入,比如按個回車鍵,服務器才會繼續執行
 88 
 89     // write initial values to OPC params
 90     for( double x =0.;  x< 50.;x+=.1 ) {
 91         server.WriteValue( "Values.int1", FILETIME_NULL, 192, CComVariant( sin(x) ) );
 92         server.WriteValue( "Values.int2", FILETIME_NULL, 192, CComVariant( cos(x) ) );
 93         Sleep(100);
 94     }
 95 
 96     srand( (unsigned)time( NULL ) );
 97 
 98     for(int i=0;i<10000;++i) {
 99         sprintf(str,"RandomValues.int%d",i+1);
100         server.WriteValue( str , FILETIME_NULL, 192, CComVariant( rand() ) );
101     }
102 
103     printf("\t waiting return for close server \n");
104     gets(str);  // 同樣是等待用戶在控制台的任意輸入,服務器結束服務
105 
106     server.StopServer();
107 
108 
109     CoUninitialize();
110 
111     fclose(pfFile);
112     FreeConsole();
113 
114     return 0;
115 }
116 
117 

其實解決方案就是通過控制台輸入參數來區分進程啟動后進入注冊流程還是處理流程,同時為了調試方便,並能夠讓我看到客戶端遠程啟動服務器的實際效果,我還為服務器分配了一個輸出控制台(缺省情況下OPC后台啟動是看不到交互窗口的),這樣服務器一旦被客戶端啟動,輸出控制台將在遠程機器上彈出,我們就可以看到服務器輸出的調試信息了,是不是很酷!至此三個問題解決,workshop庫的樣例服務器可以正常工作了。

最后,已經調整完且測試通過的workshop庫VS2010的源碼工程還是在我的github倉庫獲取:

https://github.com/Neo-T/OPCDASrvBasedOnLightOPC


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM