프린터 서버로 동작하는 라즈베리 파이에 온도, 습도를 측정하는 dht11 센서를 달았다. 센서는 1,500원인데 배송비 포함 4,000원에 구매했다. 인터넷에서 바로 사용할 수 있는 python 코드를 찾았지만 동작하지 않아 gcc로 된 코드를 사용했다. 아래 그림과 같이 구성했다.
111에러가 홈 서버 접속 시도를 차단했다. 찾아보니 서버 내 my.cnf 파일 bind 설정을 수정해야 함을 알았다.
워드 프레스 테마 디렉토리 안 functions.php를 보면 사용자 정의 함수를 만들고, 데이터베이스를 조회할 수 있다. functions.php 파일을 수정하기 보다, header.php를 간단하게 수정했다. 스타일 등 html을 잘 모르기 때문에 가장 간단한 정보만 표시했다. 아래는 header.php 파일이다. 마지막에 4줄 정도만 넣었다.
<?php
/**
* The header for our theme
*
* This is the template that displays all of the <head> section and everything up until <div id="content">
*
* @link https://developer.wordpress.org/themes/basics/template-files/#template-partials
*
* @package WordPress
* @subpackage Twenty_Seventeen
* @since Twenty Seventeen 1.0
* @version 1.0
*/
?><!DOCTYPE html>
<html <?php language_attributes(); ?> class="no-js no-svg">
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="profile" href="https://gmpg.org/xfn/11">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<div id="page" class="site">
<a class="skip-link screen-reader-text" href="#content"><?php _e( 'Skip to content', 'twentyseventeen' ); ?></a>
<header id="masthead" class="site-header" role="banner">
<?php get_template_part( 'template-parts/header/header', 'image' ); ?>
<?php if ( has_nav_menu( 'top' ) ) : ?>
<div class="navigation-top">
<div class="wrap">
<?php get_template_part( 'template-parts/navigation/navigation', 'top' ); ?>
</div><!-- .wrap -->
</div><!-- .navigation-top -->
<?php endif; ?>
</header><!-- #masthead -->
<?php
/*
* If a regular post or page, and not the front page, show the featured image.
* Using get_queried_object_id() here since the $post global may not be set before a call to the_post().
*/
if ( ( is_single() || ( is_page() && ! twentyseventeen_is_frontpage() ) ) && has_post_thumbnail( get_queried_object_id() ) ) :
echo '<div class="single-featured-image-header">';
echo get_the_post_thumbnail( get_queried_object_id(), 'twentyseventeen-featured-image' );
echo '</div><!-- .single-featured-image-header -->';
endif;
?>
<div class="site-content-contain">
<div id="content" class="site-content">
/*여기 추가*/
<div align="center">
<?php
$mydb = new wpdb('????','?????','???','???');$results = $mydb->get_results("SELECT * FROM `dataTemperatureAndHumidity` ORDER BY `dataTemperatureAndHumidity`.`time` DESC limit 1");foreach($results as $result){echo "온도: "; echo $result->temperature; echo ", 습도: "; echo $result->humidity;echo ", 수집시각: ";echo $result->time;}
?></div>
다음은 라즈베리안에서 돌아가는 gcc 파일이다. 인터넷 파일 그대로 사용했고, crobtab으로 30분에 한번 실행하도록 했다. 다음 컴파일 할 때 mysql과 wiringPi 옵션을 주어야 한다.
/* mysql connect and query sample */
#include <stdio.h>
#include <stdlib.h>
#include <mysql.h>
#include <time.h>
#include <unistd.h>
#include <wiringPi.h>
#include <stdint.h>
#define MAXTIMINGS 83
#define DHTPIN 0
int dht11_dat[5] = {0, } ;
void read_dht11_dat()
{
uint8_t laststate = HIGH ;
uint8_t counter = 0 ;
uint8_t j = 0, i ;
uint8_t flag = HIGH ;
uint8_t state = 0 ;
float f ;
dht11_dat[0] = dht11_dat[1] = dht11_dat[2] = dht11_dat[3] = dht11_dat[4] = 0 ;
pinMode(DHTPIN, OUTPUT) ;
digitalWrite(DHTPIN, LOW) ;
delay(18) ;
digitalWrite(DHTPIN, HIGH) ;
delayMicroseconds(30) ;
pinMode(DHTPIN, INPUT) ;
for (i = 0; i < MAXTIMINGS; i++) {
counter = 0 ;
while ( digitalRead(DHTPIN) == laststate) {
counter++ ;
delayMicroseconds(1) ;
if (counter == 200) break ;
}
laststate = digitalRead(DHTPIN) ;
if (counter == 200) break ; // if while breaked by timer, break for
if ((i >= 4) && (i % 2 == 0)) {
dht11_dat[j / 8] <<= 1 ;
if (counter > 20) dht11_dat[j / 8] |= 1 ;
j++ ;
}
}
if ((j >= 40) && (dht11_dat[4] == ((dht11_dat[0] + dht11_dat[1] + dht11_dat[2] +
dht11_dat[3]) & 0xff))) {
printf("humidity = %d.%d %% Temperature = %d.%d *C \n", dht11_dat[0],
dht11_dat[1], dht11_dat[2], dht11_dat[3]) ;
}
else printf("Data get failed\n") ;
}
float RandomFloat(float a, float b) {
//seed 값 현재 시각으로 초기화
srand(time(NULL));
float random = ((float) rand()) / (float) RAND_MAX;
float diff = b - a;
float r = random * diff;
return a + r;
}
int main(int argc, char **argv)
{
if (wiringPiSetup() == -1) exit(1) ;
char temp[10];
char humidity[10];
read_dht11_dat();
sprintf(temp, "%d.%d", dht11_dat[2], dht11_dat[3]);
sprintf(humidity, "%d.%d",dht11_dat[0], dht11_dat[1]);
MYSQL mysql;
MYSQL *conn;
MYSQL_RES *result;
MYSQL_ROW row;
//printf("now: %d-%d-%d %d:%d:%d\n",tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday,tm.tm_hour,tm.tm_min, tm.tm_sec);
char query_buffer[2048];
conn = mysql_init(&mysql);
//float temper, centerVal; //온도, 중심값.
//centerVal=25;
//한글을 사용하기 위해 utf-8로 설정.
mysql_options(conn, MYSQL_SET_CHARSET_NAME, "utf8");
mysql_options(conn, MYSQL_INIT_COMMAND, "SET NAMES utf8");
//18, 19행은 한글 사용하기 위해, mysql 옵션 수정..
//아래는 서버 설정에 맞도록 수정..
if(!mysql_real_connect(conn, "????", "????", "????", NULL, 3306, NULL, 0)){
fprintf(stderr,"error %s", mysql_error(conn));
printf("cannot connect");
exit(1);
}
else{
//아래는 test_db를 사용하도록 수정..
if (mysql_select_db(conn, "????")){
printf("cannot use databases");
exit(1);
}
}
time_t t =time(NULL);
struct tm tm = *localtime(&t);
sprintf(query_buffer, "select * from dataTemperatureAndHumidity");
mysql_query(conn, query_buffer);
result = mysql_store_result(conn);
while( (row = mysql_fetch_row(result)) != NULL){
printf("row[0],%s, %s, %s",row[0], row[1], row[2]);
}
sprintf(query_buffer, "INSERT INTO `dataTemperatureAndHumidity`(`time`, `temperature`, `humidity`) VALUES ('%d-%d-%d %d:%d:%d', %s, %s);",tm.tm_year+1900,tm.tm_mon+1,tm.tm_mday,tm.tm_hour,tm.tm_min, tm.tm_sec, temp, humidity);
if (mysql_query(conn, query_buffer)){
printf("query faild : %s\n", query_buffer);
exit(1);
}
mysql_close(conn);
}
처음 설정한 목표를 드디어 수행했다. 서버를 대략 구현했으니 다음으로 클라이언트를 만들었다. 내가 쪼랩이라 tutorial 문서 그대로 사용했다. cilent는 UA_Client_Service_browse로 server가 어떤 데이터를 가지고 있는지 볼 수 있다. 그러나 역시 한 단계밖에 볼 수 없다. 하부 구조를 보려면 nodeId를 기억하여 다시 browse를 해야 한다. 더 좋은 방법이 있겠지만, 이것도 되니까 문제 안된다.
서버가 method를 가지고 있어 client에서 오는 콜을 받아 정리해서 보내 줄 수도 있지만, 시간을 많이 쓸 듯하여 쉽고 간단하게 갔다.
서버가 각 공정 동작 상황을 OPC UA nodeId에 기록한다. 5초마다 업데이트 했는데, 실재 PLC로 구현한다면 매 초마다 update하는 방식으로 해야 할 듯하다. 그리 많은 부하가 걸리지는 않을 듯 하다.
클라이언트는 매 3초마다 서버로 request하여 정보를 받아오는 방식으로 작성했다. timestamp는 server쪽 시각을 쓰지 않고, 클라이언트가 임의로 만든 시각을 기록했다.
데이터를 데이터 베이스로 바로 업데이트 한다던가, 파일로 기록할 수도 있다. 그러나 시간을 많이 쓰므로 표준 출력으로 나온 메세지를 file로 redirect하면 쉽게 할 수 있다. 중복 데이터는 sort로 쉽게 지울 수 있다. bash 만세!
임의 설비에 latch, clamp, pin, robot을 설치했다. 대부분 래더로 동작하고, PLC 메이커가 cpu에 opc ua 서버 기능을 구현하여 주기적으로 plc 메모리 데이터를 server로 업데이트 한다.(고 생각하자.) 집에 PLC가 없고, 아직 이를 제대로 구현할지 모르겠어 대충 짜집기로 만들었다.
object로 각 설비를 등록하고 Value 변수로 staus를 bool 타입으로 등록했다. OPC UA 문서를 읽지 못해 이렇게 하는게 맞는지 모르겠다. 암튼 4개(pin, latch, clamp, robot) 를 등록하고 순서대로 동작하도록 했다.
처음에는 thread로 돌려 UA_Server_writeValue(server, myEquipNodeId.pinNodeId, value);로 써 주려했으나, 유투브에 좋은 동영상을 찾아 그대로 따라했다. 서버 중간에 sleep을 넣어 버리면 client 쪽 연결도 응답하지 못한다. timestamp를 찍어 일정 조건을 넘어가면 update 하도록 (따라)했다. 항상 cycle time이 똑같을 수 없으니, random 값으로 조정 해야겠다.
이렇게 하는게 맞는지 모르겠다. 나중에 client에서 접속하면 안되는건 아니겠지. 다음 단계는 client에서 node를 아니 받아서 mysql에 업로드하면 된다. 이제 라즈베리 파이로 입력 8, 출력 4점 PLC + OPC UA 서버를 만들 수 있다. 신뢰성은 0이어서 의미 없겠지만.
open62541이 OPC UA 규격에 맞춰 browse 기능을 지원한다. OPC UA 문서에 어떻게 사용하는지 알 수 있는데, 멤버만 볼 수 있다. 일단 대충 필요한 실린더를 구성했다 하자. 상용 PLC 메이커가 OPC UA 기능을 구현한다면 내부에 server 기능으로 넣을 듯 하다. 지금 구할 수 없으니, 대충 비슷하게 만들고 싶다. 여기서 문제를 알았다. 한번 Variable을 할당하면 어떻게 바꾸지? callback으로 바꿀 수 있는데, nodeId를 알아야 한다. nodeId를 null로 설정하면 server가 임의로 할당한다.
아래 그림 NodeID i=50235, Status Value를 True로 만들고 싶다. 어떻게??
이럴때 browse를 사용한다.(아마도..) sample로 구현되어 있는데, example 디렉토리 tutorial_server_object.c에 있다. 가장 간단한 방법이 아래와 같다.
#define TEST
#ifdef TEST
//테스트
//각 설비값을 설정하는 부분
UA_Variant tmp_value;
bool bitoff = 0;
UA_Variant_setScalar(&tmp_value, &bitoff, &UA_TYPES[UA_TYPES_BOOLEAN]);
UA_NodeId tmpNodeId = UA_NODEID_STRING(1, "Status");
UA_Server_writeValue(server, tmpNodeId, tmp_value);
/* Find the NodeId of the status child variable */
UA_RelativePathElement rpe;
UA_RelativePathElement_init(&rpe);
rpe.referenceTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT);
rpe.isInverse = false;
rpe.includeSubtypes = true;
//rpe.targetName = UA_QUALIFIEDNAME(1, "Status");
rpe.targetName = UA_QUALIFIEDNAME(1, "latch");
//rpe.targetName = UA_QUALIFIEDNAME(1, "variable");
UA_BrowsePath bp;
UA_BrowsePath_init(&bp);
UA_NodeId *nodeId = UA_NodeId_new();
*nodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
//UA_NodeId tmp = {0, UA_NODEIDTYPE_NUMERIC, {50238}};
//printf("\nNode id is %d\n", tmp);
//UA_NodeId *nodeId = &tmp;
bp.startingNode = *nodeId;
bp.relativePath.elementsSize = 1;
bp.relativePath.elements = &rpe;
UA_BrowsePathResult bpr =
UA_Server_translateBrowsePathToNodeIds(server, &bp);
if(bpr.statusCode != UA_STATUSCODE_GOOD ||
bpr.targetsSize < 1)
return bpr.statusCode;
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "found target is %d", bpr.targets->targetId.nodeId.identifier.numeric);
//print로 structure를 출력할 수 없음.
//printf("\nnode id is %d\n", (UA_NodeId)bpr.targets->targetId.nodeId);
//* Set the status value */
UA_Boolean status = true;
UA_Variant value;
UA_Variant_setScalar(&value, &status, &UA_TYPES[UA_TYPES_BOOLEAN]);
UA_Server_writeValue(server, bpr.targets->targetId.nodeId, value);
//UA_Server_writeValue(server, tmp, value);
UA_BrowsePathResult_clear(&bpr);
#endif
여기를 gdb로 보면 시작 위치를 설정, 알고 있는 정보를 입력, 두 구조체를 비교하여 찾는다. 찾으면 0을 반환한다.
어디에서 시작할 지 정해줘야 한다. 가장 상위인 Object에서 시작했다. NodeId i=84다. 이 번호도 정했을 것 같다. 문제는 이 설정이 어떻게 되어 있는지 Objects 바로 아래 항목만 찾는다. 위 방식으로 NodeId i=50234까지는 찾았다. 그러나 Varible이 없어 write를 하더라도 효과 없다. 하위 항목인 1:Status, NodeId i=50235를 찾아야 했다. 편법으로 시작점을 50243으로 넣어 줬다.
#define TEST
#ifdef TEST
//테스트
//각 설비값을 설정하는 부분
UA_Variant tmp_value;
bool bitoff = 0;
UA_Variant_setScalar(&tmp_value, &bitoff, &UA_TYPES[UA_TYPES_BOOLEAN]);
UA_NodeId tmpNodeId = UA_NODEID_STRING(1, "Status");
UA_Server_writeValue(server, tmpNodeId, tmp_value);
/* Find the NodeId of the status child variable */
UA_RelativePathElement rpe;
UA_RelativePathElement_init(&rpe);
rpe.referenceTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT);
rpe.isInverse = false;
rpe.includeSubtypes = true;
//rpe.targetName = UA_QUALIFIEDNAME(1, "Status");
rpe.targetName = UA_QUALIFIEDNAME(1, "latch");
//rpe.targetName = UA_QUALIFIEDNAME(1, "variable");
UA_BrowsePath bp;
UA_BrowsePath_init(&bp);
UA_NodeId *nodeId = UA_NodeId_new();
*nodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
//UA_NodeId tmp = {0, UA_NODEIDTYPE_NUMERIC, {50238}};
//printf("\nNode id is %d\n", tmp);
//UA_NodeId *nodeId = &tmp;
bp.startingNode = *nodeId;
bp.relativePath.elementsSize = 1;
bp.relativePath.elements = &rpe;
UA_BrowsePathResult bpr =
UA_Server_translateBrowsePathToNodeIds(server, &bp);
if(bpr.statusCode != UA_STATUSCODE_GOOD ||
bpr.targetsSize < 1)
return bpr.statusCode;
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "found target is %d", bpr.targets->targetId.nodeId.identifier.numeric);
//print로 structure를 출력할 수 없음.
//printf("\nnode id is %d\n", (UA_NodeId)bpr.targets->targetId.nodeId);
//* Set the status value */
UA_Boolean status = true;
UA_Variant value;
UA_Variant_setScalar(&value, &status, &UA_TYPES[UA_TYPES_BOOLEAN]);
UA_Server_writeValue(server, bpr.targets->targetId.nodeId, value);
//UA_Server_writeValue(server, tmp, value);
//다시 아래로 내림.
bp.startingNode = bpr.targets->targetId.nodeId;
rpe.targetName = UA_QUALIFIEDNAME(1, "Status");
UA_BrowsePathResult bpr2 =
UA_Server_translateBrowsePathToNodeIds(server, &bp);
if(bpr2.statusCode != UA_STATUSCODE_GOOD ||
bpr2.targetsSize < 1)
return bpr.statusCode;
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "found target is %d", bpr2.targets->targetId.nodeId.identifier.numeric);
UA_Server_writeValue(server, bpr2.targets->targetId.nodeId, value);
UA_BrowsePathResult_clear(&bpr);
#endif
Variable을 제대로 수정한다. 이제 rucursive browse를 어떻게 해야 하는지 알아야겠다. 삽질해도 보람차다.ㅠㅠ