Posting

Machbase의 최신 소식을 지금 만나보세요

[MACHBASE 활용] 타이어 불균형 측정

사물인터넷의 등장으로 사물의 데이터 생산량이 기하급수적으로 증가하면서 빅데이터의 처리가 업계의 화두로 등장했습니다. 4차 산업혁명 시대로 진입하여 모든 사물이 인터넷으로 연결되고 스마트시티(스마트교통, 스마트환경, 스마트헬스, 스마트안전 등) 데이터가 증가하고 있는 이때, IoT 기반 센서 데이터를 제대로 관리할 수 있는 서비스가 필요합니다. 이에 맞춰 저희 회사에서 시계열 데이터베이스를 기반으로 IoT 데이터 처리에 대한 관리ㆍ융합ㆍ활용의 시각을 제공하는 책을 새로 발간했습니다. 이를 통해 IoT DB 데이터 전문가가 될 수 있는 지름길을 마련해보십시오!

이 책은 시계열 데이터베이스를 통해 IoT, AI, big data, Cloud 등 디지털 기술의 융합과 제조, 에너지 등 저희 회사 제품인 Machbase를 소개하고 기본적인 설치 방법과 각종 활용법을 소개하고 있습니다. 그 중 17장 “마크베이스와 C/C++의 연동”(page 294 ~ 319)에는 제목 그대로 C/C++ 소스에서 마크베이스에 데이터를 입력하는 방법에 대한 설명이 실려 있습니다. 이 내용을 좀더 자세히 풀어보고자 합니다. 소개할 내용은 센서값을 측정해서 Machbase에 저장하고, 그 측정값을 불러와 원하는 결과를 계산하는 방법입니다.

센서로 무엇을 측정해 볼까?

타이어 편심
  • 그림 1. 타이어 무게중심 마크

혹시 자동차 타이어에서 위와 같은 마크를 보신 적이 있으십니까? 노란색 마크는 타이어에서 가장 가벼운 위치를 나타내며, 빨간색 마크는 타이어에서 원심력이 가장 강한 위치를 나타냅니다. 그리고 휠의 하얀 마크는 휠에서 가장 가벼운 위치입니다. 즉, 타이어의 빨간 마크와 휠의 흰 마크를 일치시키면 바퀴의 균형이 가장 잘 맞을 가능성이 높아집니다. 만일 타이어에 빨간 마크가 없다면 노란 마크를 휠의 공기밸브 위치에 맞추어 주면 된다고 합니다.

그림 1.을 보시면 노란 마크와 빨간 마크가 인접해 있죠. 가장 가벼운 위치인데 어떻게 원심력이 더 강할 수 있는지 의문이 들 수도 있는데, 타이어가 완벽한 원형이 아니며, 가볍더라도 약간 더 바깥쪽으로 찌그러져 있다면 해당 위치의 원심력이 더 강해질 수 있습니다. 타이어 회전속도가 올라갈수록 중심에서 더 먼 부분의 원심력이 더 강해지겠죠.

타이어를 수평으로 놓고 무게중심을 측정한 결과를 정적 밸런스(static balance)라고 합니다. 이 때 무게중심의 반대 위치에 노란 마크를 찍어주면 됩니다. 반면 원심력이 가장 강한 위치인 빨간 마크는 타이어를 회전시켜보아야 측정할 수 있습니다. 이렇게 타이어를 회전시켜 보아야 알 수 있는 결과를 통틀어 동적 밸런스(dynamic balance)라고 합니다. 동적 밸런스를 측정할 때에는 타이어를 축에 장착해서 공기까지 주입하고 직접 회전시켜서 측정합니다. 이 밸런스가 잘 맞지 않으면 타이어 자체가 회전하면서 상하좌우로 진동해 운전할 때 핸들이 떨리고 연비가 낮아지며 차체의 피로도가 올라갑니다.

  • 그림 2. 미국 MicroPoise 사의 타이어 동적 밸런스 측정장치

이런 기계에 타이어를 물린 다음 모터를 회전시켜서 측정합니다. 타이어 공장에는 생산 라인에 이러한 기계가 여러 대 늘어서서 생산 라인에서 나오는 타이어의 동적 밸런스를 확인합니다. 원심력이 가장 강한 부위에 빨간색 마크를 찍어주며, 품질 기준에서 벗어나는 타이어는 불량품 취급하여 생산 라인에서 제거해 주기도 합니다.

카센터에서도 자체적으로 회전하는 밸런싱 기계를 들여와서 휠 얼라인먼트를 맞추거나 타이어 교체, 위치교환 등을 수행할 때 휠을 차체에서 분리해 밸런스를 측정하고 조정하는 작업도 해 준다고 합니다. 타이어와 휠을 조립한 후에 축에 물려서 회전시켜 밸런스를 측정하고 납 추를 휠에 부착하여 불균형을 제거해 줍니다. 검색엔진에서 “휠밸런스 측정” 등으로 검색해보면 회전축에 바퀴를 물리고 돌리는 장면을 심심치 않게 볼 수 있습니다.

편심 측정기

점질량 m이 각속도 ω로 반지름 r을 그리면서 회전할 때 원심력의 크기는 F=mrω2입니다. 타이어의 편심을 일종의 점질량으로 가정한다면 F의 크기를 알아내서 타이어의 동적 불균형 정도를 계산할 수 있습니다. 그런데 현실의 타이어는 두께가 있는 원통형이며, 회전축에 수직방향인 편심뿐 아니라 회전축 방향으로 힘을 가하는 편심도 존재합니다. 하지만 이런 부분까지 고려하려면 내용이 너무 방대해지니 최대한 단순화하여 생각합니다. 이러한 분야는 기계공학 중에서도 진동 분석에 대한 지식이 필요합니다.

타이어의 편심을 측정할 때에는 보통 측정장치를 이렇게 구성합니다.

  • 그림 3. 밸런스 측정기 개략도

파란색인 엔코더는 현재 모터의 회전 각도를 알아내는 센서입니다. 1024단계 엔코더라면 출력값을 1024로 나눈 나머지가 현재 엔코더의 위치, 즉 모터 축의 방향이 됩니다. 빨간색인 로드셀은 해당 위치에 얼마만큼의 힘이 가해지는지를 알아내는 센서입니다. 로드셀 여러 개를 활용하고 행렬 연산을 활용하여 위에 설명한 타이어 두께 방향의 편심도 알아낼 수는 있습니다만 본 예제에서는 로드셀을 하나만 사용하여 수평방향 편심만을 측정하는 것으로 구성하겠습니다.

센서값으로 측정 대상의 값 알아내기
  • 그림 4. 측정 대상

우리에게 필요한 값은 원심력의 크기 F와 위치 θ입니다. F로는 타이어의 양/불 여부를, θ는 빨간 마크를 찍어줄 위치를 결정합니다.

결론부터 말하자면, 타이어 편심의 크기 F와 위치 θ는 아래 수식으로 구할 수 있습니다.

센서 측정값이 f[] 배열에 들어 있고 엔코더의 출력값이 [0, R)이며 N바퀴 회전시킨 센서 값을 기반으로 계산한다고 하면 

  • 수식 1.

입니다. 이 장의 나머지 부분은 왜 위와 같은 식이 되는지에 대한 설명인데, 건너뛰고 다음 장인 “측정값을 바탕으로 편심을 계산하고 Machbase에 저장해 보자” 부분을 바로 읽어도 됩니다. 클릭하면 이동합니다.

  • 그림 5. 원운동시 x축 방향의 원심력 크기

원심력 F를 가지는 회전체가 등속 원운동을 지속할 때, 회전축을 z축이라고 생각하면 x축, 또는 y축 방향에서 바라본 원심력의 크기는 사인파 곡선을 그립니다. 정확히는 그림 4.와 같은 구성일 때 로드셀에 들어오는 힘의 크기는 아래와 같습니다.

  • 수식 2

그러면 측정값의 최댓값, 최솟값을 구해 F로 삼고 시작 값을 역삼각함수에 넣어서 θ를 구하면 될 것처럼 보입니다.

그런데 세상 일이 그렇게 간단하지 않습니다. 센서의 오차도 있고 전원으로 인한 노이즈가 측정값에 섞여 들어오기 때문에 실제 로드셀로 측정한 파형은 이렇게 나옵니다.

  • 그림 6. 실제 로드셀 출력값

붉은색 그래프가 참값이라면 실제 측정값은 푸른색 그래프처럼 얻어집니다. 여기서 최솟값, 최댓값을 구하면 참값에 비해 지나치게 큰 값을 구할수밖에 없습니다. 그리고 시작값도 정확히 θ를 가리키는지 알 수 없습니다. 즉 측정값의 노이즈를 적당히 뭉개주면서 평균치를 구하는 방법이 필요합니다. 다행히 로드셀의 결과값들은 일정한 주기성을 띠고 있기에 푸리에 급수를 활용해 알아낼 수 있습니다. 푸리에 급수는 주기 T를 지닌 복잡하지만 반복되는 파동을 단순한 파동, 즉 삼각함수 여러 개로 나타내는 방법입니다. 수식으로 나타내면 아래와 같습니다.

  • 수식 3.

모터에 장착된 엔코더 출력값으로 주기를 알 수 있으며, 측정값의 수열이 있으니 이제 ab 수열의 값을 알아내면 됩니다. 현재 우리가 관심있는 값은 초기값인 a0와 가장 큰 주기를 가지고 진동하는 a1b1 값입니다. 나머지 값은 무시합니다. 그러면…

  • 수식 4.

이 식을 얻습니다. 푸리에 급수의 특성에 의해

  • 수식 5

입니다. 그런데 본래의 함수를 생각해보면 a0는 cos 함수를 [0, 2π] 구간에서 적분한 값이기 때문에 그냥 0이 됩니다. 따라서 a0 항도 무시할 수 있습니다. 결국

  • 수식 6

이라는 수식을 얻을 수 있습니다. (a1b1을 편의상 ab로 표기합니다.) 삼각함수의 덧셈정리에 의해서 

  • 수식 7

이 됩니다. 결론적으로

  • 수식 8

이라는 것을 알 수 있습니다. a와 b는 푸리에 급수로 구할 수 있으니 필요한 값을 모두 찾을 수 있습니다.

우리가 가지고 있는 자료는 함수가 아니라 측정값의 배열인데, 이 때는 적분 대신 그냥 합을 사용하면 됩니다.

측정값을 바탕으로 편심을 계산하고 Machbase에 저장해 보자

본 예제는 모두  https://github.com/MACHBASE/sensor-eccentricity-example에서 다운로드받을 수 있습니다.

파일 설명과 예제 빌드하기

예제는 Machbase 서버, 혹은 Machbase 라이브러리가 설치된 리눅스 서버에서 컴파일해 테스트할 수 있습니다. 예제를 다운로드받으면 sensor-eccentricity-example 디렉터리에 아래 파일들이 생성됩니다. 예제 디렉터리에서 make를 수행하면 measure와 retrieve, 두 개의 실행파일을 만들어 줍니다.

  • Makefile
  • crt.sql: 테이블 생성 쿼리
  • sel.sql: 예제 조회 쿼리
  • drop.sql: 테이블 삭제 쿼리
  • measure.c: 샘플 측정 데이터를 생성해 Machbase에 입력하는 예제 코드
  • retrieve.c: 샘플 측정 데이터를 Machbase에서 조회하여 가져오는 예제 코드
$ ls Makefile README.md crt.sql drop.sql measure.c retrieve.c sel.sql 
$ make gcc -O2 -g -o measure measure.c -I/home/djin/work/nfx/machbase_home/include -L/home/djin/work/nfx/machbase_home/lib -lmachbasecli -lm -lpthread -ldl -lrt gcc -O2 -g -o retrieve retrieve.c -I/home/djin/work/nfx/machbase_home/include -L/home/djin/work/nfx/machbase_home/lib -lmachbasecli -lm -lpthread -ldl -lrt 
$

measure는 센서 측정을 흉내내서 샘플 센서값을 생성하고 그 센서값들을 Machbase에 입력하는 예제입니다. retrieve는 Machbase에서 측정값과 센서값 목록을 불러오는 역할을 합니다. 아주 간단히 테스트하려면 아래처럼 수행해 보면 됩니다.

$ machsql
Mach> @crt.sql
Mach> CREATE TABLE measure_list
(
    line_id     VARCHAR(40),
    tire_model  VARCHAR(40),
    ins_time    DATETIME,
    real_ecc    DOUBLE,
    real_dir    DOUBLE,
    eccentric   DOUBLE,
    direction   DOUBLE
);
Created successfully.
Elapsed time: 0.216
Mach> CREATE TAGDATA TABLE tag
(
    line_id         VARCHAR(40) PRIMARY KEY,
    tick_time       DATETIME BASETIME,
    sensor_value    DOUBLE SUMMARIZED,
    encoder_value   INT
);
Executed successfully.
Elapsed time: 3.131
Mach> quit
 
 
$ ./measure
Connect using "DSN=127.0.0.1;UID=SYS;PWD=*****;CONNTYPE=1;PORT_NO=28000"...success!
Preparing for append...success!
Append [MACH01-BASE01 [2019-05-30 21:10:47]][3.3][92.8125]...success!
 
 
$ machsql
Mach> @sel.sql
Mach> SELECT * FROM measure_list;
LINE_ID                                   TIRE_MODEL                                INS_TIME                       
------------------------------------------------------------------------------------------------------------------------
REAL_ECC                    REAL_DIR                    ECCENTRIC                   DIRECTION                  
---------------------------------------------------------------------------------------------------------------------
MACH01-BASE01                             IOT-TS-205/70-ZR-18                       2019-05-30 21:10:47 000:000:000
3.3                         92.8125                     3.31298                     93.3698                    
[1] row(s) selected.
Elapsed time: 0.001
Mach> SELECT line_id, COUNT(sensor_value) FROM tag GROUP BY line_id;
line_id                                   COUNT(sensor_value) 
------------------------------------------------------------------
MACH01-BASE01 [2019-05-30 21:10:47]       10240               
[1] row(s) selected.
Elapsed time: 0.019
Mach> quit
 
$ ./retrieve "MACH01-BASE01" "2019-05-30 21:10:47" 10 out.txt
Connect using "DSN=127.0.0.1;UID=SYS;PWD=*****;CONNTYPE=1;PORT_NO=28000"...success!
Retrieve from measure_list...success!
    Retrieving...
    "MACH01-BASE01 [2019-05-30 21:10:47]" [3.31298][93.3698] => [10] records retrieved
 
$ cat out.txt
0   -1.63072    -0.194738
0.000976562 -1.0865 -0.174442
0.00195312  1.56414 -0.154139
0.00292969  -0.0316268  -0.13383
0.00390625  -1.20739    -0.113516
0.00488281  -0.919142   -0.0931976
0.00585938  -0.394896   -0.0728758
0.00683594  -0.868248   -0.0525513
0.0078125   0.2832  -0.0322248
0.00878906  -0.862152   -0.0118971
 
$

crt.sql로 테이블을 생성한 후 measure를 실행하면 측정값을 하나 만들어서 measure_list 테이블과 tag 테이블에 입력해 줍니다. 이후 sel.sql로 조회 대상을 알아내고 retrieve로 센서 데이터를 가져올 수 있습니다. retrieve에는 sel.sql로 조회한 measure_list 테이블에서 line_id와 ins_time을 인수로 주어 실행하면 됩니다. ins_time은 measure가 실행될 때 현재 시각을 자동으로 가져오기 때문에 실행할 때마다 달라집니다.

테이블 구조

crt.sql을 사용해 테이블 두 개를 생성합니다. 공장의 라인 번호와 측정한 시각, 측정결과를 저장하기 위한 measure_list, 센서값 자체를 저장하기 위한 TAGDATA 테이블인 tag입니다.

CREATE TABLE measure_list
(
    line_id     VARCHAR(40),
    tire_model  VARCHAR(40),
    ins_time    DATETIME,
    real_ecc    DOUBLE,
    real_dir    DOUBLE,
    eccentric   DOUBLE,
    direction   DOUBLE
);

CREATE TAGDATA TABLE tag
(
    line_id         VARCHAR(40) PRIMARY KEY,
    tick_time       DATETIME BASETIME,
    sensor_value    DOUBLE SUMMARIZED,
    encoder_value   INT
);
  • measure_list 테이블 컬럼 설명
    • line_id: 생산라인 ID
    • tire_model: 당시 검사한 타이어의 모델
    • ins_time: 검사시각
    • real_ecc: 참값 원심력 크기
    • real_dir: 참값 원심력 방향 (도)
    • eccentric: 측정값 원심력 크기
    • direction: 측정값 원심력 방향 (도)
  • tag 테이블 컬럼 설명
    • line_idmeasure_list의 line_id와 ins_time을 결합한 측정결과 ID
    • tick_time: 개별 센서값 측정 당시 시각
    • sensor_value: 센서 측정값
    • encoder_value: 개별 센서값 측정 당시 엔코더의 값

센서 데이터를 읽어와 Machbase에 저장 – measure.c

코드 설명

우선 main 함수의 코드를 살펴보겠습니다. 본문 내의 코드는 의사코드(pseudocode)가 일부 포함되어 있으며, 오류 확인 등의 일부 기능은 제외되어 있습니다.

double sig[TOTAL];
time_t ticks[TOTAL];
double sResMag, sResAng;
 
if( openDBConn("127.0.0.1", "SYS", "MANAGER", 28000) != 0 )
{
    printf("Open DB connection failed.\n");
    exit(-1);
}
if( prepareAppend() != 0 )
{
    printf("Preparing for APPEND failed.\n");
    exit(-1);
}

while not stop
{
    sec = now();
    if( measure(sig, ticks, TOTAL, &sResMag, &sResAng) != 0 )
    {
        printf("Measure failed.\n");
        break;
    }
    if( appendMeasure(sLineID, sTireModel, sec, sig, ticks, TOTAL, sResMag, sResAng) != 0 )
    {
        printf("Append failed.\n");
        break;
    }
}

closeDBConn();

데이터 저장용으로 사용할 서버의 주소와 포트, 사용자 ID와 비밀번호를 이용해 openDBConn 함수를 호출해 Machbase에 연결합니다. prepareAppend 함수로 테이블에 입력할 준비를 마칩니다. 그 후 measure 함수로 센서 값을 측정하여 appendMeasure 함수로 데이터베이스에 입력하는 과정을 반복하다가 중단할 때가 되면 closeDBConn 함수를 호출해 Machbase와 연결을 닫고 종료합니다. 실제로 사용하려면 openDBConn 함수에 인수로 넘겨주는 Machbase 서버의 주소와 사용자 ID, 비밀번호, 접속 포트를 변경해 주면 됩니다.

sig에는 로드셀의 측정값이, ticks에는 측정 당시의 시각이 나노초 단위로 들어갑니다. sResMag와 sResAng는 위에서 설명한 F와 θ입니다. 

measure 함수는 300RPM으로 회전하는 타이어에서 총 10바퀴에 해당하는 측정값 10,240개를 double 배열인 sig에 넣어줍니다. 엔코더 값은 [0, 1024)입니다. 300RPM, 즉 1초간 5회전이기에 10바퀴이면 측정 시간은 2초가 됩니다.

int measure(double* aSig, time_t* aTicks, int aCount, double* aResMag, double* aResAng)
{
    double noiselevel = 2.;
    int i;
    double sRealMag;
    double sRealAng;
    struct timeval tm;
    time_t sec;
    double sXPos;
    double sYPos;
    double sResMag;
    double sResAng;
 
 
    /* simulate sensor value collection */
    sRealMag = 3. + (double)(rand() % 30) / 10.;
    sRealAng = (double)(rand() % 1024);
    if( gettimeofday(&tm, NULL) == 0 )
    {
        sec = tm.tv_sec;
    }
    else
    {
        printf("Get time failed.\n");
        return -1;
    }
    sec *= 1000000000;
    for(i = 0; i < aCount; i++)
    {
        aTicks[i]   = sec + (i * (time_t)2000000000) / aCount;
        aSig[i]     = sRealMag * cos(((i % ROTATE) - sRealAng) * PI / ROTATE * 2) +
                      ((noiselevel * ((rand() % 5001) - 2500)) / 2500.);
    }

    /* calculate magnitude and angle of eccentric */
    sXPos = 0;
    sYPos = 0;
    for(i = 0; i < aCount; i++)
    {
        sXPos += aSig[i] * cos(i * 2 * PI / ROTATE);
        sYPos += aSig[i] * sin(i * 2 * PI / ROTATE);
    }
 
    sXPos   = sXPos * 2 / aCount;
    sYPos   = sYPos * 2 / aCount;
    sResMag = sqrt(sXPos * sXPos + sYPos * sYPos);
    sResAng = atan2(sYPos, sXPos) * 180 / PI;
    if( sResAng < 0. ) sResAng += 360.;
    *aResMag = sResMag;
    *aResAng = sResAng;
    return 0;
}

예제로 작성한 코드에서는 센서 측정을 흉내내기 위해 F와 θ의 참값을 난수로 생성해서 cos 함수와 결합해 적당한 값에 노이즈를 더해 sig 배열에 넣어줍니다. 실제로 작성할 때에는 모터의 엔코더와 로드셀에 연결된 ADC 보드에서 읽어온 값을 넣어 주면 됩니다. 물론 부하가 0일 때 로드셀에서 출력하는 전압을 측정해 영점조정하고 빈 축을 회전시켜서 축 자체의 편심을 계산하고, 측정값에서 빼 주는 보상 작업 등이 필요합니다. 그리고 측정한 시각도 나노초 단위로 aTicks 배열에 넣어 줍니다. 실제로 작성할 때에는 모터 엔코더 신호의 rising edge, 혹은 falling edge에서 일관되게 센서 값과 현재 시각을 구해 오면 됩니다.

이후 측정해온 센서값을 바탕으로 F와 θ의 측정값을 계산합니다. 상단에서 설명한 계산식이 그대로 들어가 있는 것을 볼 수 있습니다.

DB에 센서 값을 입력하기 위한 준비과정과 DB 연결을 닫는 부분입니다.

SQLHENV   gEnv;
SQLHDBC   gConn;
SQLHSTMT  gMeasureStmt;
SQLHSTMT  gSensorsStmt;
 
int openDBConn(char* aHost, char* aUser, char* aPWD, int aPort)
{
    char sConnStr[128];
    if( SQLAllocEnv(&gEnv) == SQL_ERROR )
    {
        printf("Cannot allocate SQLENV");
        return -1;
    }
    if( SQLAllocConnect(gEnv, &gConn) == SQL_ERROR )
    {
        printf("Cannot allocate SQLConn" );
        return -1;
    }
    snprintf(sConnStr, 128, "DSN=%s;UID=%s;PWD=%s;CONNTYPE=1;PORT_NO=%d",
             aHost, aUser, "*****", aPort);
    printf("Connect using \"%s\"...", sConnStr);
    snprintf(sConnStr, 128, "DSN=%s;UID=%s;PWD=%s;CONNTYPE=1;PORT_NO=%d",
             aHost, aUser, aPWD, aPort);
    SQLDriverConnect(gConn, NULL, sConnStr, SQL_NTS, NULL, 0, NULL, SQL_DRIVER_NOPROMPT);
    printf("success!\n");
    return 0;
}
 
int prepareAppend(void)
{
    printf("Preparing for append...");
    SQLAllocStmt(gConn, &gMeasureStmt);
    SQLAllocStmt(gConn, &gSensorsStmt);
 
    if( SQLAppendOpen(gMeasureStmt, "measure_list", DEFAULT_CHECK_COUNT) == SQL_ERROR )
    {
        printf("appendOpen measure list failed\n");
        return -1;
    }
    if( SQLAppendOpen(gSensorsStmt, "tag", DEFAULT_CHECK_COUNT) == SQL_ERROR )
    {
        printf("appendOpen tag data failed\n");
        return -1;
    }
    printf("success!\n");
    return 0;
}

void closeDBConn(void)
{
    VERIFYERROR( SQLFreeStmt(gSensorsStmt, SQL_DROP), "Free stmt failed" );
    VERIFYERROR( SQLFreeStmt(gMeasureStmt, SQL_DROP), "Free stmt failed" );
    VERIFYERROR( SQLDisconnect(gConn), "Disconnect failed" );
    VERIFYERROR( SQLFreeConnect(gConn), "Free session failed" );
    VERIFYERROR( SQLFreeEnv(gEnv), "Free env failed" );
}

openDBConn 함수에서 데이터베이스에 연결하는 부분을 볼 수 있습니다. 그리고 prepareAppend 함수에서 스테이트먼트를 할당하고 SQLAppendOpen으로 데이터를 추가할 테이블을 열어 두는 것을 볼 수 있습니다. closeDBConn 함수에서는 열어둔 스테이트먼트와 Machbase 연결 핸들을 모두 닫고 해제합니다.

measure_list 테이블과 tag 테이블에 측정 결과와 센서값을 추가하는 코드입니다.

int appendMeasure(char*     aLineID,
                  char*     aModel,
                  time_t    aSec,
                  double*   aSig,
                  time_t*   aTicks,
                  int       aCount,
                  double    aRealMag,
                  double    aRealAng,
                  double    aResMag,
                  double    aResAng)
{
    int     i;
    time_t  sNSec;
    struct tm   sLocalTime;
    char        sTagStr[40];
    SQL_APPEND_PARAM    sMesParam[7];
    SQL_APPEND_PARAM    sSenParam[4];
    localtime_r(&aSec, &sLocalTime);
    sNSec = aSec * 1000000000;
    snprintf(sTagStr, sizeof(sTagStr),
             "%s [%04d-%02d-%02d %02d:%02d:%02d]",
             aLineID,
             sLocalTime.tm_year + 1900,
             sLocalTime.tm_mon + 1,
             sLocalTime.tm_mday,
             sLocalTime.tm_hour,
             sLocalTime.tm_min,
             sLocalTime.tm_sec);
    printf("Append [%s][%g][%g]...", sTagStr, aRealMag, aRealAng);
    sSenParam[0].mVarchar.mLength   = strlen(sTagStr);
    sSenParam[0].mVarchar.mData     = sTagStr;
    for(i = 0; i < TOTAL; i++)
    {
        sSenParam[1].mDateTime.mTime    = aTicks[i];
        sSenParam[2].mDouble            = aSig[i];
        sSenParam[3].mInteger           = i % ROTATE;
        VERIFYERROR( SQLAppendDataV2(gSensorsStmt, sSenParam), "failed" );
    }
    VERIFYERROR( SQLAppendFlush(gSensorsStmt), "failed" );
    sMesParam[0].mVarchar.mLength   = strlen(aLineID);
    sMesParam[0].mVarchar.mData     = aLineID;
    sMesParam[1].mVarchar.mLength   = strlen(aModel);
    sMesParam[1].mVarchar.mData     = aModel;
    sMesParam[2].mDateTime.mTime    = sNSec;
    sMesParam[3].mDouble            = aRealMag;
    sMesParam[4].mDouble            = aRealAng;
    sMesParam[5].mDouble            = aResMag;
    sMesParam[6].mDouble            = aResAng;
    VERIFYERROR( SQLAppendDataV2(gMeasureStmt, sMesParam), "failed" );
    VERIFYERROR( SQLAppendFlush(gMeasureStmt), "failed" );
    printf("success!\n");
    return 0;
}

SQL_APPEND_PARAM 배열에 값을 채워서 SQLAppendDataV2 함수로 센서 데이터를 입력합니다. 우선 생산라인 ID와 측정 시각을 결합해 단일한 측정 ID를 만들어 줍니다. 그리고 측정 ID와 측정값, 시각 배열, 모터 엔코더 값을 tag 테이블에 입력합니다. 입력을 완료하면 10,240개의 신규 레코드가 tag 테이블에 추가됩니다. 그 이후 생산라인 ID와 타이어 모델명, 측정시각과 측정값을 measure_list 테이블에 입력해 둡니다. 지금은 진짜 센서로 측정하는 것이 아니기 때문에 비교 용도로 흉내낸 값을 만드는데 쓰인 참값도 테이블에 입력해 줍니다. 엔코더 값 역시 본래는 measure 함수에서 값을 넣어 주어야 하지만, 여기에서는 그냥 나머지 값만을 사용합니다. Machbase에서는 시각값을 나노초 단위로 입력받기 때문에 초단위 시각에 1,000,000,000을 곱해서 입력하는 것을 볼 수 있습니다.

입력을 완료한 후에는 SQLAppendFlush를 호출해 데이터를 Machbase에 완전히 밀어넣어 줍니다.

실행 결과

시험삼아 다섯 개의 측정 결과를 입력해 보았습니다.

$ ./measure 5
Connect using "DSN=127.0.0.1;UID=SYS;PWD=*****;CONNTYPE=1;PORT_NO=28000"...success!
Preparing for append...success!
Append [MACH01-BASE01 [2019-05-30 19:12:49]][3.1][82.9688]...success!
Append [MACH01-BASE01 [2019-05-30 19:12:51]][5.5][347.695]...success!
Append [MACH01-BASE01 [2019-05-30 19:12:53]][3][118.828]...success!
Append [MACH01-BASE01 [2019-05-30 19:12:55]][5.7][103.711]...success!
Append [MACH01-BASE01 [2019-05-30 19:12:57]][5.5][276.68]...success!
 
 
$ machsql -f sel.sql
Mach> SELECT * FROM measure_list;
LINE_ID                                   TIRE_MODEL                                INS_TIME                       
------------------------------------------------------------------------------------------------------------------------
REAL_ECC                    REAL_DIR                    ECCENTRIC                   DIRECTION                  
---------------------------------------------------------------------------------------------------------------------
MACH01-BASE01                             IOT-TS-205/70-ZR-18                       2019-05-30 19:12:57 000:000:000
5.5                         276.68                      5.494                       276.81                     
MACH01-BASE01                             IOT-TS-205/70-ZR-18                       2019-05-30 19:12:55 000:000:000
5.7                         103.711                     5.69708                     103.677                    
MACH01-BASE01                             IOT-TS-205/70-ZR-18                       2019-05-30 19:12:53 000:000:000
3                           118.828                     3.00509                     119.044                    
MACH01-BASE01                             IOT-TS-205/70-ZR-18                       2019-05-30 19:12:51 000:000:000
5.5                         347.695                     5.50631                     347.382                    
MACH01-BASE01                             IOT-TS-205/70-ZR-18                       2019-05-30 19:12:49 000:000:000
3.1                         82.9688                     3.1126                      82.8543                    
[5] row(s) selected.
Elapsed time: 0.001
 
 
Mach> SELECT line_id, COUNT(sensor_value) FROM tag GROUP BY line_id;
line_id                                   COUNT(sensor_value) 
------------------------------------------------------------------
MACH01-BASE01 [2019-05-30 19:12:51]       10240               
MACH01-BASE01 [2019-05-30 19:12:53]       10240               
MACH01-BASE01 [2019-05-30 19:12:55]       10240               
MACH01-BASE01 [2019-05-30 19:12:49]       10240               
MACH01-BASE01 [2019-05-30 19:12:57]       10240               
[5] row(s) selected.
Elapsed time: 0.094

참값과 상당히 근사한 측정값이 들어가 있죠? tag 테이블에도 line_id 별로 측정결과 10,240개가 잘 입력되어 있습니다.

Machbase에서 센서 데이터를 불러오기 - retrieve.c

코드 설명

측정값을 입력한 후에는 Machbase에서 측정결과를 조회할 수 있어야 합니다. 생산라인 ID와 측정 일시로 센서데이터를 불러오는 법을 살펴보겠습니다. 아래의 SQL문으로 measure_list와 tag 테이블에서 측정결과와 센서데이터를 불러올 수 있습니다.

SELECT eccentric, direction FROM measure_list WHERE line_id=? AND ins_time=TO_DATE(?);
SELECT encoder_value, sensor_value FROM tag WHERE line_id=? ORDER BY tick_time;

우선 Machbase에 연결하는 부분은 measure.c와 큰 차이가 없습니다. 스테이트먼트를 열고 데이터를 읽어오는 부분만 보아도 충분합니다.

measure_list에서 라인 ID와 날짜 기준으로 측정값을 읽어오는 코드입니다.

char* gMeasureSQL = "SELECT eccentric, direction FROM measure_list WHERE line_id=? AND ins_time=TO_DATE(?)";
double  sMag;
double  sAng;
 
printf("Retrieve from measure_list...");
VERIFYERROR( SQLAllocStmt(gConn, &gMeasureStmt),
             gConn, NULL,
             "measure statement alloc failed");
VERIFYERROR( SQLPrepare(gMeasureStmt, (SQLCHAR*)gMeasureSQL, SQL_NTS),
             gConn, gMeasureStmt,
             "prepare failed" );
sLineIDLen = strlen(aLineID);
sTimeLen = strlen(aTimeStr);
VERIFYERROR( SQLBindParameter(gMeasureStmt,
                              1,
                              SQL_PARAM_INPUT,
                              SQL_C_CHAR,
                              SQL_VARCHAR,
                              0,
                              0,
                              aLineID,
                              40,
                              &sLineIDLen),
             gConn, gMeasureStmt,
             "bind parameter failed" );
VERIFYERROR( SQLBindParameter(gMeasureStmt,
                              2,
                              SQL_PARAM_INPUT,
                              SQL_C_CHAR,
                              SQL_VARCHAR,
                              0,
                              0,
                              aTimeStr,
                              40,
                              &sTimeLen),
             gConn, gMeasureStmt,
             "bind parameter failed" );
VERIFYERROR( SQLExecute(gMeasureStmt), gConn, gMeasureStmt, "execute failed" );
VERIFYERROR( SQLBindCol(gMeasureStmt, 1, SQL_C_DOUBLE, &sMag, 0, 0),
             gConn, gMeasureStmt,
             "bind column failed" );
VERIFYERROR( SQLBindCol(gMeasureStmt, 2, SQL_C_DOUBLE, &sAng, 0, 0),
             gConn, gMeasureStmt,
             "bind column failed" );
printf("success!\n");
 
VERIFYERROR( SQLFetch(gMeasureStmt), gConn, gMeasureStmt, "fetch failed" );

SELECT를 위한 스테이트먼트를 열고, SQL에 인수를 지정하기 위해 SQLPrepare로 수행 준비를 합니다. 조건절에 들어갈 기준이 line_id와 ins_time 두 개이니, SQLBindParameter로 조건 두 개를 준비된 SQL에 연결해 줍니다. 그리고 SQLExecute로 실행. 조회에 성공하면 SQLBindCol로 조회 결과를 확인할 변수들을 연결합니다. 마지막으로 SQLFetch를 호출하면 SQLBindCol로 연결한 변수에 조회해온 결과값이 들어가게 됩니다. 위 코드에서는 F를 저장하기 위한 sMag와 θ를 저장할 sAng 두 개를 연결했습니다.

measure_list에는 조건에 해당하는 레코드가 하나씩만 들어 있으니 페치를 한 번만 해도 되지만, 조회 대상 레코드가 여러 개라면 레코드를 여러 번 페치해와야 합니다. 이번에는 tag 테이블에서 센서 측정값 목록을 불러와 보겠습니다. 측정값은 line_id 하나당 10,240개가 들어 있습니다.

double  sSensorValue;
int     sEncoderValue;
 
VERIFYERROR( SQLAllocStmt(gConn, &gSensorsStmt),
        gConn, NULL,
        "sensors statement alloc failed");
VERIFYERROR( SQLPrepare(gSensorsStmt, (SQLCHAR*)gSensorsSQL, SQL_NTS),
        gConn, gSensorsStmt,
        "prepare failed" );
VERIFYERROR( SQLBindParameter(gSensorsStmt,
            1,
            SQL_PARAM_INPUT,
            SQL_C_CHAR,
            SQL_VARCHAR,
            0,
            0,
            sTagStr,
            40,
            &sTagStrLen),
        gConn, gSensorsStmt,
        "bind parameter failed" );
VERIFYERROR( SQLExecute(gSensorsStmt), gConn, gSensorsStmt, "execute failed" );
VERIFYERROR( SQLBindCol(gSensorsStmt, 1, SQL_C_LONG,
            &sEncoderValue, 0, NULL),
        gConn, gSensorsStmt,
        "bind column failed" );
VERIFYERROR( SQLBindCol(gSensorsStmt, 2, SQL_C_DOUBLE,
            &sSensorValue, 0, NULL),
        gConn, gSensorsStmt,
        "bind column failed" );
sSensorCount = 0;
 
while( (SQLFetch(gSensorsStmt) == SQL_SUCCESS) && (aCount != 0) )
{
    sExpectedValue = sMag * cos((sEncoderValue * 2 * PI / ROTATE) - sRad);
    fprintf(fp, "%g\t%g\t%g\n",
            (sEncoderValue + (sLoops * ROTATE)) / (double)ROTATE,
            sSensorValue,
            sExpectedValue);
    if( sEncoderValue == ROTATE - 1 )
    {
        sLoops++;
    }
    sSensorCount++;
    aCount--;
}

조건이 하나이니 SQLBindParameter도 한 번만 호출해주면 됩니다. SQL을 수행하고 조회값을 저장할 변수를 연결하는 부분은 동일합니다.

이후 SQLFetch가 SQL_SUCCESS를 리턴하는동안 반복합니다. SQLFetch는 조회 결과가 남아 있으면 SQL_SUCCESS를 리턴하고, 조회 결과가 더 이상 없다면 SQL_NO_DATA를 리턴합니다. 동시에 조회 결과가 남아 있다면 SQLBindCol로 연결해둔 변수에 계속해서 조회한 값을 넣어 줍니다. 우리는 안심하고 연결한 변수를 사용하면 됩니다.

실행 결과

measure_list에서 line_id와 ins_time 하나를 지정해 센서값을 읽어와 보겠습니다.

$ ./retrieve "MACH01-BASE01" "2019-05-30 19:12:51"
Connect using "DSN=127.0.0.1;UID=SYS;PWD=*****;CONNTYPE=1;PORT_NO=28000"...success!
Retrieve from measure_list...success!
    Retrieving...
0   5.30485 5.37332
0.000976562 5.57116 5.36584
0.00195312  7.19087 5.35816
0.00292969  5.36557 5.35027
0.00390625  4.75127 5.34218
0.00488281  6.75357 5.3339
...
9.99512 3.87428 5.40769
9.99609 3.4264  5.40122
9.99707 4.02632 5.39455
9.99805 4.70363 5.38768
9.99902 6.02715 5.3806
    "MACH01-BASE01 [2019-05-30 19:12:51]" [5.50631][347.382] => [10240] records retrieved
$

데이터 10,240개가 잘 나옵니다. 이대로 보기는 좀 불편하니 10개만 불러와 볼까요? retrieve.c에는 일정 개수만 불러오고, 불러온 값을 파일에 저장하는 기능도 추가되어 있습니다.

$ ./retrieve "MACH01-BASE01" "2019-05-30 19:12:51" 10
Connect using "DSN=127.0.0.1;UID=SYS;PWD=*****;CONNTYPE=1;PORT_NO=28000"...success!
Retrieve from measure_list...success!
    Retrieving...
0   5.30485 5.37332
0.000976562 5.57116 5.36584
0.00195312  7.19087 5.35816
0.00292969  5.36557 5.35027
0.00390625  4.75127 5.34218
0.00488281  6.75357 5.3339
0.00585938  3.36047 5.32541
0.00683594  4.17117 5.31672
0.0078125   5.13127 5.30783
0.00878906  6.75677 5.29874
    "MACH01-BASE01 [2019-05-30 19:12:51]" [5.50631][347.382] => [10] records retrieved
 
$

별도의 파일로 출력도 가능합니다. 레코드 개수를 -1로 설정하면 데이터를 모두 불러올 수 있습니다. 데이터 전체를 가져와 그래프로 그려 보았습니다. x축은 회전수, y축은 센서 측정값입니다.

$  ./retrieve "MACH01-BASE01" "2019-05-30 19:12:51" -1 02.out
Connect using "DSN=127.0.0.1;UID=SYS;PWD=*****;CONNTYPE=1;PORT_NO=28000"...success!
Retrieve from measure_list...success!
    Retrieving...
    "MACH01-BASE01 [2019-05-30 19:12:51]" [5.50631][347.382] => [10240] records retrieved
$
  • 그림 7. 전체 데이터 그래프. 

파란색 그래프는 측정값(을 흉내낸 값)으로 실제로 센서에서 출력해준 값입니다. 빨간색 그래프는 센서 데이터를 바탕으로 계산한 F와 θ 결과치의 그래프입니다.

레코드 10,240개를 전부 그래프에 그렸더니 보기가 좀 불편합니다. 맨 앞의 두 바퀴 분량인 레코드 2,048개만 조회해서 그려봅니다.

$ ./retrieve "MACH01-BASE01" "2019-05-30 19:12:51" 2048 01.out
Connect using "DSN=127.0.0.1;UID=SYS;PWD=*****;CONNTYPE=1;PORT_NO=28000"...success!
Retrieve from measure_list...success!
    Retrieving...
    "MACH01-BASE01 [2019-05-30 19:12:51]" [5.50631][347.382] => [2048] records retrieved
 
$
  • 그림 8. 두 바퀴 분량만 그린 그래프.

인코더값 없이 tick_time 컬럼과 sensor_value  컬럼 데이터만으로 푸리에 변환하여 절대값만을 뽑아낸 결과 그래프입니다. measure.c에서 입력한대로 5Hz, 즉 300RPM 근처에서 가장 높은 값을 보이고 있습니다. 위의 retrieve.c를 살짝만 수정하면 됩니다. 관심있으신 분들은 직접 구현해 보셔도 좋을듯합니다.

  • 그림 9. 센서데이터의 푸리에 변환 결과

위의 그래프들은 리눅스용 그래프 유틸리티인 gnuplot을 사용해서 그렸습니다. 실무에서 활용할 때에는 엑셀 등에 데이터를 넣고 차트로 뽑아내거나, 더 멋진 화면을 보여주는 전용 뷰어를 제작해도 좋겠습니다. 분석을 위해서 MATLAB 등에 입력 데이터로 활용할 수도 있습니다.

맺음말

마크베이스는 시계열 데이터베이스로서 초고속으로 시계열 데이터를 저장, 추출, 분석하기 위한 목적으로 개발되었습니다. 특히, 마크베이스는 고성능의 데이터 처리를 목적으로 하기 때문에 컴파일 언어인 C를 기반으로 개발되었고, 전통적인 관계혀7ㅇ 데이터베이스 엔진의 구조와는 차별되는 새로운 아키텍처로 설계되었습니다. 이번 글을 통해 Machbase에 센서데이터를 입력하고, 또 그 센서 데이터를 조회해서 여러가지 출력을 얻어내는 방법을 살펴보았으며 센서 데이터를 측정함으로써 이상탐지와 예지보전 분야에 도움이 되기를 기대하며 이번 테크 블로그는 여기서 줄입니다.

연관 포스트

C언어로 Binary data를 Machbase에 넣기

1.개요 Data를 다루다 보면 numeric, varchar 형 데이터뿐만 아니라 JPG, PNG, MP3와 같은 Binary data도 database에 저장해야 할 때가 존재한다. 그러나 일반 data들과는 달리 Binary

[MACHBASE 연동] Android studio에서 JDBC 연결하기

마크베이스 기술지원본부 이현민 1. 개요 수많은 데이터들이 많은 환경에서 생성되고 있는 오늘날, 우리 현대인들의 동반자인 스마트폰 또한 데이터생성의 주체로써 또는 전달자로서 알게 모르게 그 구실을