2018年1月31日 星期三

OpENer學習心得(一):理解範例程式碼

基本的OpENer程式已經可以運行啦~真是可喜可賀!
/* 其實並沒有QAQQQQQQQQ */
接下來大家最常做的動作應該就是看懂範例程式main.c的架構!

愚鈍如我,一開始我覺得main.c裡面根本就沒有什麼好看的,給的東西少的讓人崩潰,但我就這樣仔細看…仔細看…仔細看……終於讓我在崩潰了兩天之後,看出了門道!!

讓原本已死心的我,在嘗試開始自己建立CIP Class的時候,發現了原來這份範例程式碼已經有建立了第一個!!

接下來就讓我來說說我看出來的心得,有錯誤歡迎指教>A<!


首先,找到OpENer的Target Stack
http://eipstackgroup.github.io/OpENer/index.html

接著,找到Porting OpENer的部分點進去。
Target Stack首頁直接拉下去,Further Topics的第一項,或著左手邊目錄的地方也找的到。

Startup Sequence:
第一個大標題,說明的是一個基本的EtherNet/IP Adapter要啟動所需要的步驟。
這5個步驟非常有助於理解main.c的程式碼!!

下面是我有加了一些printf,讓我理解這支程式現在到底在幹嘛的code:
/*******************************************************************************
 * Copyright (c) 2009, Rockwell Automation, Inc.
 * All rights reserved.
 *
 ******************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

#include "generic_networkhandler.h"
#include "opener_api.h"
#include "cipcommon.h"
#include "trace.h"

/******************************************************************************/
/** @brief Signal handler function for ending stack execution
 *
 * @param signal the signal we received
 */
void LeaveStack(int signal);

/*****************************************************************************/
/** @brief Flag indicating if the stack should end its execution
 */
int g_end_stack = 0;

/******************************************************************************/

int main(int argc, char *arg[]) {

  EipUint8 my_mac_address[6];
  EipUint16 unique_connection_id;

  /* 判斷參數數量合不合法 */
  if (argc != 12) {
    printf("Wrong number of command line parameters!\n");
    printf("The correct command line parameters are:\n");
    printf(
      "./OpENer ipaddress subnetmask gateway domainname hostaddress macaddress\n");
    printf(
      "    e.g. ./OpENer 192.168.0.2 255.255.255.0 192.168.0.1 test.com testdevice 00 15 C5 BF D0 87\n");
    exit(0);
  } else {
    /* 參數數量對了之後,開始建立Adapter */
    /* fetch Internet address info from the platform */

    /* Startup Sequence的第1點--
     * Configure the network properties :
     *   EIP_STATUS ConfigureNetworkInterface(const char *ip_address, const char *subnet_mask, const char *gateway_address)
     *   void ConfigureMACAddress(const EIP_UINT8 *mac_address)
     *   void ConfigureDomainName(const char *domain_name)
     *   void ConfigureHostName(const char *host_name)
     */

    /* setup the data of the network interface needed */
    ConfigureNetworkInterface(arg[1], arg[2], arg[3]);
    printf("IP:%s\nSubnetmask:%s\nGateway:%s\n",arg[1],arg[2],arg[3]);
    printf("Setup network data success!\n");
    printf("\n----\n\n");

    ConfigureDomainName(arg[4]);
    printf("Domain name:%s\n",arg[4]);
    printf("Setup network domain name success!\n");
    printf("\n----\n\n");

    ConfigureHostName(arg[5]);
    printf("Host name:%s\n",arg[5]);
    printf("Setup network host name success!\n");
    printf("\n----\n\n");

    my_mac_address[0] = (EipUint8) strtoul(arg[6], NULL, 16);
    my_mac_address[1] = (EipUint8) strtoul(arg[7], NULL, 16);
    my_mac_address[2] = (EipUint8) strtoul(arg[8], NULL, 16);
    my_mac_address[3] = (EipUint8) strtoul(arg[9], NULL, 16);
    my_mac_address[4] = (EipUint8) strtoul(arg[10], NULL, 16);
    my_mac_address[5] = (EipUint8) strtoul(arg[11], NULL, 16);
    ConfigureMacAddress(my_mac_address);
    printf("Mac address:%s %s %s %s %s %s\n",arg[5],arg[6],arg[7],arg[8],arg[9],arg[10]);
    printf("Setup mac address success!\n");
    printf("\n----\n\n");

  }

  /* Startup Sequence的第2點--
   * Set the device's serial number :
   *   void setDeviceSerialNumber(EIP_UINT32 serial_number)
   */
  /*for a real device the serial number should be unique per device */
  SetDeviceSerialNumber(123456789);
  printf("Device serial number:123456789\n");
  printf("Setup serial number success!\n");
  printf("\n----\n\n");

  /* nUniqueConnectionID should be sufficiently random or incremented and stored
   *  in non-volatile memory each time the device boots.
   */
  unique_connection_id = rand();

  /* Startup Sequence的第3點--
   * Initialize OpENer :
   *   CipStackInit(EIP_UINT16 unique_connection_id)
   */
  /* Startup Sequence的第4點--
   * Create Application Specific CIP Objects :
   *   Within the call-back function ApplicationInitialization(void)
   *   or
   *   after CipStackInit(void) has finished
   */
  /* 這個function裡面建立了第一個Object!下面會獨立拉開來說明!
   * 除了初始化OpENer Structure,裡面也呼叫了ApplicationInitialization(void)
   * 如果想要建立自己特定應用的CIP Class,
   * 可以在CipStackInit(void)之後建立,或著在ApplicationInitialization(void)裡面進行創建
   * p.s ApplicationInitialization(void) :
   * 該函數可以在/source/src/ports/POSIX/sample_application資料夾下的sampleapplication.c進行修改
   */
  /* Setup the CIP Layer */
  CipStackInit(unique_connection_id);
  printf("Connection ID:%d\n",unique_connection_id);
  printf("Setup connection ID success!\n");
  printf("\n----\n\n");

  /* 另一個可以創建自己特定應用CIP Class的位置 */
  /* Can create object here */

  /* Startup Sequence的第5點--
   * Setup the listening TCP and UDP port :
   *   configure your network library so that TCP and UDP messages on port will be received
   */
  /* TCP和UDP的socket設置在NetworkHandlerInitialize()進行處理
   * OpENer的初始port是設爲44818,即0xAF12,通常用來接收EtherNet/IP的explicit messages */
  /* Setup Network Handles */
  if ( kEipStatusOk == NetworkHandlerInitialize() ) {
    g_end_stack = 0;
#ifndef WIN32
    /* register for closing signals so that we can trigger the stack to end */
    signal(SIGHUP, LeaveStack);
#endif

    /* 我自己設來顯示進入while迴圈用的參數 */
    int flag = 1;
    /* The event loop. Put other processing you need done continualy in here */
    while (1 != g_end_stack) {

      if(flag){
        printf("Here is ready to do job!\n\n");
        flag = 0;
      }

      if ( kEipStatusOk != NetworkHandlerProcessOnce() ) {
        OPENER_TRACE_ERR("Error in NetworkHandler loop! Exiting OpENer!\n");
        break;
      }
    }

    /* clean up network state */
    NetworkHandlerFinish();
  }
  /* close remaining sessions and connections, cleanup used data */
  ShutdownCipStack();

  return -1;
}

void LeaveStack(int signal) {
  (void) signal; /* kill unused parameter warning */
  OPENER_TRACE_STATE("got signal HUP\n");
  g_end_stack = 1;
}


接下來是比較重要的地方,如果想自己創建CIP Class的話,這邊可以當作一個範本~
Function:
void CipStackInit(const EipUint16 unique_connection_id)
Location:
/source/src/cip/cipcommon.c
void CipStackInit(const EipUint16 unique_connection_id) {
  EncapsulationInit();

  /* MessageRouter是第一個建立的CIP Object!
   * CipMessageRouterInit()裡面就是基本的建立CIP Obect的流程,不過是比較簡陋的版本XD
   */
  /* The message router is the first CIP object be initialized!!! */
  EipStatus eip_status = CipMessageRouterInit();
  OPENER_ASSERT(kEipStatusOk == eip_status);

  /* 下面幾個function是用來初始化CIP Stack的函數~
   * 也就是OpENer建立時,一些default的CIP Object!
   * 算是幾個也能當作創建CIP Object的範例
   */
  eip_status = CipIdentityInit();
  OPENER_ASSERT(kEipStatusOk == eip_status);
  eip_status = CipTcpIpInterfaceInit();
  OPENER_ASSERT(kEipStatusOk == eip_status);
  eip_status = CipEthernetLinkInit();
  OPENER_ASSERT(kEipStatusOk == eip_status);
  eip_status = ConnectionManagerInit(unique_connection_id);
  OPENER_ASSERT(kEipStatusOk == eip_status);
  eip_status = CipAssemblyInitialize();
  OPENER_ASSERT(kEipStatusOk == eip_status);

  /* 這裡就是上面說的可以實作自己CIP Object的地方
   * 不過我還沒有看很懂,所以就先放置了QQ
   */
  /* the application has to be initialized at last */
  eip_status = ApplicationInitialization();
  OPENER_ASSERT(kEipStatusOk == eip_status);

  /* Shut up compiler warning with traces disabled */
  (void) eip_status;
}

好了,過度的function先看過去就好O3O
主要學習建立CIP Object的內容要看下面這個函數!
Function:
EipStatus CipMessageRouterInit()
Location:
/source/src/cip/cipmessagerouter.c
EipStatus CipMessageRouterInit() {

  /* 建立一個message router的CIP Class */
  /*
   * CipClass* CreateCipClass ( const EipUint32     class_id,
   *                            const int           number_of_class_attributes,
   *                            const EipUint32     highest_class_attribute_number,
   *                            const int           number_of_class_services,
   *                            const int           number_of_instance_attributes,
   *                            const EipUint32     highest_instance_attribute_number,
   *                            const int           number_of_instance_services,
   *                            const int           number_of_instances,
   *                            char                *name,
   *                            const EipUint16     revision,
   *                            void(*)(CipClass *) InitializeCipClass )
   */
  CipClass *message_router = CreateCipClass(kCipMessageRouterClassCode, /* class ID : 給class一個編號 */
                                            0, /* # of class attributes : class attribute初始的數量,設爲0即一開始沒有初始化class attribute */
                                            7, /* # highest class attribute number : class attribute能夠設置的最大編號 */
                                            2, /* # of class services : 初始化的class service數量 */
                                            0, /* # of instance attributes : 初始化的instance attribut數量,設爲0即一開始沒有初始化instance attribute */
                                            4, /* # highest instance attribute number : instance attribute能夠設置的最大編號 */
                                            1, /* # of instance services : 初始化的instance service數量 */
                                            1, /* # of instances : instance初始化的數量 */
                                            "message router", /* class name : 給class設置一個名字 */
                                            1, /* # class revision : 這個參數沒搞懂過QQ */
                                            NULL); /* # function pointer for initialization : 指定一個初始化的function
                                                    * 如果設爲NULL,會自動加入7個class attribute
                                                    * 推測是初始化class和instance之後,可以設置一個function來針對兩者進行詳細設置
                                                    * 範本可以參考放在/source/src/cip下的cipidentity.c
                                                    * void InitializeCipIdentity(CipClass*)是個專門為創建identity這個class寫的函數
                                                    * 內容是寫一些特別要放在identity class下的attribute,然後塞給identity class這樣
                                                    */

  /* 判斷Class是否create成功 */
  if (NULL == message_router) {
    /* create失敗的話,就是CIP Object初始化失敗了 */
    return kEipStatusError;
  }

  /* Class初始化成功後,設置Class Service */
  /* void InsertService ( const CipClass *const    cip_class_to_add_service,
   *                      const EipUint8           service_code,
   *                      const CipServiceFunction service_function,
   *                      char *const              service_name ) 
   */
  InsertService(message_router, /* 要加入此Service的Class */
                kGetAttributeSingle, /* 給此Service一個編號 */
                &GetAttributeSingle, /* 實現該Service的function */
                "GetAttributeSingle"); /* 給該Service設置的名字 */

  /* g_message_router_response是在cipmessagerouter.c全域定義的一個變數
   * 目前推測是用來回應EtherNet/IP message的資料結構
   * 把參數初始化,並設定好裝data的buffer:g_message_data_reply_buffer
   */
  /* reserved for future use -> set to zero */
  g_message_router_response.reserved = 0;
  g_message_router_response.data = g_message_data_reply_buffer; /* set reply buffer, using a fixed buffer (about 100 bytes) */

  /* 全部都初始化好之後,回傳OK的EtherNet/IP State訊號 */
  return kEipStatusOk;
}

認真的扒開這個main.c,對於建立一個CIP Object終於有了一個比較確切的概念了!!ε٩(๑> ₃ <)۶з
接下來,就以這個爲基礎,開始嘗試建立一個自己的CIP Object吧!!!

3 則留言:

  1. 感謝你的文章
    沒有你的文章入手時間會再拉長很多
    我遇到了一個奇怪的現象
    向你請教
    作業系統+環境 : WIN10+mingw
    local ip:192.168.1.109
    當我按照說明文件執行
    OpENer 192.168.1.109
    總是回報 Network interface 192.168.1.109 not found.

    在一陣胡亂嘗試後
    執行 OpENer 1 成功運行了
    且利用EIP explorer -> open interface with 127.0.0.1
    終於成功抓到裝置了
    接下來繼續嘗試直到
    OpENer 10 再次成功運行了
    並利用EIP explorer -> open interface with 192.168.1.109
    又再次成功抓到裝置的資訊
    不曉得你知不知道這是為什麼?
    謝謝

    回覆刪除
    回覆
    1. 沒有在win10系統下安裝過這個,這幾天嘗試了一下mingw,被win10的網路問題搞的頭疼,make老是卡在很多相關參數宣告找不到,所以決定放棄了XD
      如果你用mingw編譯成功的話,執行的程式碼就是放在\source\src\ports\MINGW裡的main.c編出來的執行檔
      在68行那裡是判斷ip的地方,從這邊入手,可以找到為什麼ip這樣判斷不合法
      我認為問題應該在於windows系統和linux系統在ip函數庫這塊定義不一樣的緣故,windows有另外自己定義的函數庫跟參數,這部分windows有說明書可以看,這邊給你附上:https://docs.microsoft.com/zh-tw/windows/win32/iphlp/ip-helper-start-page
      個人覺得linux系統就比較好懂,反正就是數bit,比較直觀XDDD
      而windows則是把它們宣告成了各種參數,要搭著說明書自己研究一下!

      最後最後,你是用Visual Studio運行的嗎?因為你的參數設置看起來比較像是用Visual Studio運行下的參數XD
      mingw是把library裝好,設置成環境變數後,透過windows的命令提示字元視窗直接編譯運行這樣,執行方式不一樣,你可以再確認一下README~

      刪除
  2. 我是根據Opener說明文件
    使用他提供的批次檔
    setup_mingw.bat "-DOpENer_TRACES:BOOL=TRUE"
    就可以成功了
    我也嘗試過自己建立專案來編譯
    但是老卡在一個關鍵字 RESTRICT
    opener程式內適用大寫的RESTRICT
    但是我查到的C 編譯器定義這關鍵字時 都是用小寫的
    所以老是編譯不了
    只有使用他提供的批次檔才能成功編譯
    這樣的缺點就是無法在IDE的環境下偵錯
    只能用fprintf慢慢偵錯
    謝謝你的回覆

    回覆刪除