Table of Contents

مقدمه

مکانیزم ارتباطی استاندارد OPC-UA از ابزارهایی است که در صنعت همچنان کاربرد داشته و برای ارتباط تجهیزات صنعتی با نرم افزارهای اسکادا(SCADA) و حتی برخی پلتفرم های ابری اینترنت اشیاء مورد استفاده قرار می گیرد.

در این مقاله قصد داریم یک پروژه کوچک جهت پایش دما و رطوبت محیط به همراه قابلیت کنترل رله جهت کنترل AirConditioner را توسط OPC-UA ایجاد نماییم. 

در گام نخست سخت افزاری متشکل از سنسور دما و رطوبت به همراه رله را با استفاده از ماژول ها و هست رزبری(Raspberry Pi) پرومیک آماده می کنیم. سپس یک سرور  حداقلی OPC-UA برروی رزبری پای بالا می آوریم. در گام بعدی خوانش اطلاعات سنسور دما و رطوبت توسط OPC-UA و کنترل رله توسط OPC-UA را به آن می افزاییم.

پس با ما همراه باشید.

اقلام مورد نیاز برای ساخت

برای شروع نیاز به اقلام زیر داریم:

  1. کامپیوتر رزبری پای به همراه کابل شبکه و USB مناسب جهت ارتباط
  2. هت رزبری پای پرومیک(ProMake Raspberry Pi HAT)
  3. ماژول ProMake Sensor Tag(برای اندازگیری دما، رطوبت نسبی)
  4. ماژول ProMake 1-Ch Relay (برای کنترل کولر گازی یا هر مورد دیگر)

در این پروژه می توانیم از ماژول رله دو کانال هم استفاده کنیم اما باید دقت کنیم جریان عبوری از رله ها با جریانی نامی آنها تناسب داشته باشد.  این اقلام را مطابق شکل رو برو برروی هم سوار نمایید.

برای آشنایی بیشتر با محصولات به کار رفته در این کیت برروی آنها کلیک کنید

snstag-001 ( janbi )

Sensor TAG

ماژولی کوچک با سنسورهای متنوع

کامپایل و اجرای OPC-UA Server برروی رزبری پای با استفاده از open62541

به جهت اجرای سرویس OPC-UA برروی رزبری پای ابتدا می بایست بسته های پیش نیاز  را  با استفاده از دستور زیر نصب کنیم.

				
					sudo apt-get install git cmake cmake-curses-gui build-essential gcc
				
			

حال با اجرای دستورات زیر کدهای مربوط به سرور OPC-UA  متن باز  را دریافت کرده و  آماده کامپایل می کنیم.

				
					cd ~
git clone https://github.com/open62541/open62541.git
cd open62541
mkdir build
cd build
cmake ..
ccmake ..
				
			

حال یک پنجره تنظیمات را مشاهده می کنید. پس گزینه UA_BUILD_EXAMPLES را فعال کنید و با زدن دکمه c عملیات پیکربندی را انجام دهید. سپس دکمه g را برای تولید makefile بزنید.

حال با اجرای دستورات زیر کدها را کامپایل و سرور OPC-UA نمونه را اجرا نمایید.

				
					make
cd bin/examples
./tutorial_server_firststeps
				
			

ارتباط با OPC-UA Server با استفاده از یک کلاینت استاندارد

حال برای اینکه از صحت عملکرد سرور اطمینان حاصل کنید نیاز است یک Client استاندارد OPC-UA را برروی رایانه خود نصب نمایید.

ما برای این کار از Unified Automation UaExpert استفاده کردیم که یک نرم افزار رایگان است. شما می توانید از دیگر نرم افزارهای موجود و محبوب خودتان استفاده کنید. برای مشاهده برخی نرم افزارهای رایگان حوزه OPC-UA اینجا کلیک کنید.

برای شروع کافی است URI دسترسی به سرور را با قرار دادن آدرس IP بورد رزبری پای در عبارت “opc.tcp://IP-ADDRESS-OF-RASPBERRY-PI:4840” به دست آورده و مطابق شکل زیر در نرم افزار client وارد نمایید.

پس از اتصال موفق به سرور رزبری قادر خواهید بود اشیاء موجود برروی سرور OPC-UA را مشاهده کنید و متغییرهای آنها را پایش و کنترل نمایید.(در تصویر زیر زمان جاری رایانه رزبری مورد خوانش قرار گرفته است)

شخصی سازی سرور OPC-UA

برای اینکه بتوانیم سرور شخصی سازی شده خود را با استفاده از open62541 بسازیم ابتدا می بایست وارد تنظیمات کامپایل شده و گزینه‌ی UA_ENABLE_AMALGAMATION را فعال نماییم. برای این کار در پوشه build دستور زیر را اجرا می کنیم.

				
					cd ~/open62541/build/
ccmake ..
				
			

حال مطابق تصویر گزینه‌ی  UA_ENABLE_AMALGAMATION  را فعال می کنیم.

نصب کتابخانه مورد نیاز برای خوانش سنسور دما و رطوبت

برای اینکه بتوانیم داده ها را از سنسور دما و رطوبت دریافت کنیم ابتدا می بایست کتابخانه SHT را با دستورات زیر برروی رزبری دریافت کرده و build و نصب کنیم

				
					cd ~
git clone https://github.com/ondrej1024/shtlib.git
cd shtlib
make
sudo make install
				
			

نصب کتابخانه مورد نیاز برای کنترل رله

برای اینکه بتوانیم پین های GPIO متصل به ماژول رله را تحریک کنیم نیاز داریم کتابخانه wiringpi را با دستورات زیر نصب کنیم

				
					cd /tmp
wget https://project-downloads.drogon.net/wiringpi-latest.deb
sudo dpkg -i wiringpi-latest.deb
				
			

ساختن سرور OPC-UA

حال زمان نوشتن کد سرور OPC-UA برای سخت افزار است. ابتدا با دستورات زیر فایل promake_opc_ua.c را در محل پوشه build ایجاد نمایید.

				
					cd ~/open62541/build/
nano promake_server.c
				
			

حال کدهای زیر را در فایل promake_server.c قرار دهید.

				
					#include "open62541.h"

#include <signal.h>
#include <stdlib.h>

#include <wiringPi.h>
#include "sht21.h"

#define SDA_PIN 2
#define SCL_PIN 3

static UA_Boolean relay_status = false;
static UA_Float temp;
static UA_Float hum;

static int readSensor()
{
    int16_t temperature;
    uint16_t humidity;
    uint8_t err;

    /* Init the library */
    SHT21_Init(SCL_PIN, SDA_PIN);
    /* Read temperature and humidity from sensor */
    err = SHT21_Read(&temperature, &humidity);

    if (SHT21_Cleanup() != 0)
    {
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "ERROR during SHT cleanup");
        return 1;
    }

    if (err == 0)
    {
        temp = temperature / 10.0;
        hum = humidity / 10.0;
        return UA_STATUSCODE_GOOD;
    }
}

static UA_StatusCode
readTemperature(UA_Server *server,
                const UA_NodeId *sessionId, void *sessionContext,
                const UA_NodeId *nodeId, void *nodeContext,
                UA_Boolean sourceTimeStamp, const UA_NumericRange *range,
                UA_DataValue *dataValue)
{
    if (readSensor() != 0)
    {
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "ERROR during SHT reading");
        return UA_STATUSCODE_BADINTERNALERROR;
    }

    UA_Variant_setScalarCopy(&dataValue->value, &temp,
                             &UA_TYPES[UA_TYPES_FLOAT]);
    dataValue->hasValue = true;
    return UA_STATUSCODE_GOOD;
}

static UA_StatusCode
readHumidity(UA_Server *server,
             const UA_NodeId *sessionId, void *sessionContext,
             const UA_NodeId *nodeId, void *nodeContext,
             UA_Boolean sourceTimeStamp, const UA_NumericRange *range,
             UA_DataValue *dataValue)
{
    if (readSensor() != 0)
    {
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "ERROR during SHT reading");
        return UA_STATUSCODE_BADINTERNALERROR;
    }

    UA_Variant_setScalarCopy(&dataValue->value, &hum,
                             &UA_TYPES[UA_TYPES_FLOAT]);
    dataValue->hasValue = true;
    return UA_STATUSCODE_GOOD;
}

static UA_StatusCode
readRelay(UA_Server *server,
          const UA_NodeId *sessionId, void *sessionContext,
          const UA_NodeId *nodeId, void *nodeContext,
          UA_Boolean sourceTimeStamp, const UA_NumericRange *range,
          UA_DataValue *dataValue)
{

    UA_Variant_setScalarCopy(&dataValue->value, &relay_status,
                             &UA_TYPES[UA_TYPES_BOOLEAN]);
    dataValue->hasValue = true;
    return UA_STATUSCODE_GOOD;
}

static UA_StatusCode
writeRelay(UA_Server *server,
           const UA_NodeId *sessionId, void *sessionContext,
           const UA_NodeId *nodeId, void *nodeContext,
           const UA_NumericRange *range, const UA_DataValue *data)
{
    relay_status = *(UA_Boolean *)(data->value.data);
    digitalWrite(10, relay_status);

    return UA_STATUSCODE_GOOD;
}

static void
addVariables(UA_Server *server)
{
    UA_NodeId parentNodeId; // get the nodeid assigned by the server
    UA_ObjectAttributes oAttr = UA_ObjectAttributes_default;
    oAttr.displayName = UA_LOCALIZEDTEXT("en-US", "AirConditioner");
    UA_Server_addObjectNode(server, UA_NODEID_NULL,
                            UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
                            UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
                            UA_QUALIFIEDNAME(1, "AirConditioner"), UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE),
                            oAttr, NULL, &parentNodeId);

    // Define the attribute of the Temperature variable node
    UA_VariableAttributes tempAttr = UA_VariableAttributes_default;
    UA_Float myTemp = 0.1;
    UA_Variant_setScalar(&tempAttr.value, &myTemp, &UA_TYPES[UA_TYPES_FLOAT]);
    tempAttr.description = UA_LOCALIZEDTEXT("en-US", "Temperature");
    tempAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Temperature");
    tempAttr.dataType = UA_TYPES[UA_TYPES_FLOAT].typeId;
    tempAttr.accessLevel = UA_ACCESSLEVELMASK_READ;

    // Add the variable node to the information model
    UA_DataSource tempDataSource;
    tempDataSource.read = readTemperature;
    UA_Server_addDataSourceVariableNode(server, UA_NODEID_NULL, parentNodeId,
                                        UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
                                        UA_QUALIFIEDNAME(1, "Temperature"),
                                        UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), tempAttr, tempDataSource, NULL, NULL);

    // Define the attribute of the Humidity variable node
    UA_VariableAttributes humAttr = UA_VariableAttributes_default;
    UA_Float myHum = 0.1;
    UA_Variant_setScalar(&humAttr.value, &myHum, &UA_TYPES[UA_TYPES_FLOAT]);
    humAttr.description = UA_LOCALIZEDTEXT("en-US", "Humidity");
    humAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Humidity");
    humAttr.dataType = UA_TYPES[UA_TYPES_FLOAT].typeId;
    humAttr.accessLevel = UA_ACCESSLEVELMASK_READ;

    // Add the variable node to the information model
    UA_DataSource humDataSource;
    humDataSource.read = readHumidity;
    UA_Server_addDataSourceVariableNode(server, UA_NODEID_NULL, parentNodeId,
                                        UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
                                        UA_QUALIFIEDNAME(1, "Humidity"),
                                        UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), humAttr, humDataSource, NULL, NULL);

    UA_VariableAttributes statusAttr = UA_VariableAttributes_default;
    UA_Boolean status = true;
    UA_Variant_setScalar(&statusAttr.value, &status, &UA_TYPES[UA_TYPES_BOOLEAN]);
    statusAttr.description = UA_LOCALIZEDTEXT("en-US", "Status");
    statusAttr.displayName = UA_LOCALIZEDTEXT("en-US", "Status");
    statusAttr.dataType = UA_TYPES[UA_TYPES_BOOLEAN].typeId;
    statusAttr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;

    UA_DataSource statusDataSource;
    statusDataSource.read = readRelay;
    statusDataSource.write = writeRelay;
    UA_Server_addDataSourceVariableNode(server, UA_NODEID_NULL, parentNodeId,
                                        UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
                                        UA_QUALIFIEDNAME(1, "Status"),
                                        UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE), statusAttr, statusDataSource, NULL, NULL);
}

static volatile UA_Boolean running = true;
static void stopHandler(int sig)
{
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "received ctrl-c");
    running = false;
}

int main(void)
{
    signal(SIGINT, stopHandler);
    signal(SIGTERM, stopHandler);

    if (wiringPiSetup() == -1)
        return 1;

    pinMode(10, INPUT); // aka pin 24
    relay_status = digitalRead(10);
    pinMode(10, OUTPUT); // aka pin 24

    UA_Server *server = UA_Server_new();
    UA_ServerConfig_setDefault(UA_Server_getConfig(server));

    UA_ServerConfig *config = UA_Server_getConfig(server);
    config->verifyRequestTimestamp = UA_RULEHANDLING_ACCEPT;

    addVariables(server);

    UA_StatusCode retval = UA_Server_run(server, &running);

    UA_Server_delete(server);
    return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}

				
			

حال با دستور زیر فایل فوق را کامپایل کنید تا باینری سرور آماده شود. سپس آن را اجرا کنید.

				
					gcc -std=c99 open62541.c promake_server.c -lsht -lwiringPi -o promakeServer
./promakeServer
				
			

حال با استفاده از client خود به سرور متصل شوید. هم اکنون قادر خواهید بود شئ AirConditioner را مشاهده کنید که در ذیل خود متغییرهای دما و رطوبت را به صورت فقط خواندنی و متغییر وضعیت را به صورت فقط نوشتنی دارا می باشد. با تغییر مقدار متغییر status وضعیت رله نیز تغییر می کند.