Sensor TAG
ماژولی کوچک با سنسورهای متنوع
مکانیزم ارتباطی استاندارد OPC-UA از ابزارهایی است که در صنعت همچنان کاربرد داشته و برای ارتباط تجهیزات صنعتی با نرم افزارهای اسکادا(SCADA) و حتی برخی پلتفرم های ابری اینترنت اشیاء مورد استفاده قرار می گیرد.
در این مقاله قصد داریم یک پروژه کوچک جهت پایش دما و رطوبت محیط به همراه قابلیت کنترل رله جهت کنترل AirConditioner را توسط OPC-UA ایجاد نماییم.
در گام نخست سخت افزاری متشکل از سنسور دما و رطوبت به همراه رله را با استفاده از ماژول ها و هست رزبری(Raspberry Pi) پرومیک آماده می کنیم. سپس یک سرور حداقلی OPC-UA برروی رزبری پای بالا می آوریم. در گام بعدی خوانش اطلاعات سنسور دما و رطوبت توسط OPC-UA و کنترل رله توسط OPC-UA را به آن می افزاییم.
پس با ما همراه باشید.
برای شروع نیاز به اقلام زیر داریم:
در این پروژه می توانیم از ماژول رله دو کانال هم استفاده کنیم اما باید دقت کنیم جریان عبوری از رله ها با جریانی نامی آنها تناسب داشته باشد. این اقلام را مطابق شکل رو برو برروی هم سوار نمایید.
برای آشنایی بیشتر با محصولات به کار رفته در این کیت برروی آنها کلیک کنید
ماژولی کوچک با سنسورهای متنوع
ماژول رله دو کاناله
ماژول رله یک کاناله
هت منعطف و ماژولار برای رزبری و جتسون
به جهت اجرای سرویس 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
حال برای اینکه از صحت عملکرد سرور اطمینان حاصل کنید نیاز است یک Client استاندارد OPC-UA را برروی رایانه خود نصب نمایید.
ما برای این کار از Unified Automation UaExpert استفاده کردیم که یک نرم افزار رایگان است. شما می توانید از دیگر نرم افزارهای موجود و محبوب خودتان استفاده کنید. برای مشاهده برخی نرم افزارهای رایگان حوزه OPC-UA اینجا کلیک کنید.
برای شروع کافی است URI دسترسی به سرور را با قرار دادن آدرس IP بورد رزبری پای در عبارت “opc.tcp://IP-ADDRESS-OF-RASPBERRY-PI:4840” به دست آورده و مطابق شکل زیر در نرم افزار client وارد نمایید.
پس از اتصال موفق به سرور رزبری قادر خواهید بود اشیاء موجود برروی سرور 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 برای سخت افزار است. ابتدا با دستورات زیر فایل promake_opc_ua.c را در محل پوشه build ایجاد نمایید.
cd ~/open62541/build/
nano promake_server.c
حال کدهای زیر را در فایل promake_server.c قرار دهید.
#include "open62541.h"
#include
#include
#include
#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 وضعیت رله نیز تغییر می کند.