[67호]LoRa 통신 기반의 오픈소스 IoT 플랫폼을 활용한 스마트 공원 관리
2021 ICT 융합 프로젝트 공모전 최우수상
LoRa 통신 기반의 오픈소스 IoT 플랫폼을 활용한 스마트 공원 관리 : 스마트 파크(Smart Park)
글 | 숭실대학교 김성호, 이경주, 이하늘
1. 심사평
칩센 사용한 센서의 용도, 센싱 된 데이터의 전송 방법인 LoRa 무선 통신, 컨트롤을 위한 시나리오 등등이 모두 IoT 센서 솔루션을 위한 구성이 잘 되어 있는 듯합니다. 또한 목적 사용자와 적용 환경에 대한 부분도 타겟을 명확히 하여 개발의 목적과 필요한 조건의 한도를 구분한 기획 의도가 돋보입니다. 센서 환경 변화에 대하여 알림이 발생하였을 때 메일로 발송하여 확인하기 보다는 서버로 직접 전송하여, 관리 시스템의 UI 등으로 즉시 보여주면 더 좋았을것 같습니다.
펌테크 실용성, 아이디어, 창의성이 아주 돋보이는 작품으로 LoRa 통신을 기반으로 스마트 공원관리에 꼭 필요한 핵심적 요소를 효율적이면서도 최적의 시스템으로 구현했으며 전체적으로 작품 기획, 기술 구현도, 완성도 면에서 상당히 뛰어나고 훌륭한 작품이라고 생각됩니다.
위드로봇 다양한 센서와 IoT 적용에 유리한 테마파크를 현장으로 설정하여 구현한 재미있는 작품입니다.
뉴티씨 매우 훌륭한 작품입니다. 실용화가 가능한 아이디어라고 생각되며, 디자인만 좀 개선하고 프로그램 등을 좀 개선하면 좋은 제품이 될 수 있다고 생각합니다. 공원에 적용하면, 환경미화원님들이 편리하게 이용하여, 깔끔하면서도 꼭 필요할 때만 환경미화원님들이 쓰레기를 비우고, 자동으로 공원의 식물에 물을 줄 수 있고, 가로등도 제어할 수 있어서, 매우 좋은 시스템으로 생각됩니다.
엔티렉스 부설연구소 오픈소스를 이용하였지만, 시스템 구성에 맞게 시설물관리(쓰레기통, 통행량에 따른 조도 제어등)을 제어하며 관리를 할 수 있으며, 네트워크를 이용하여 여러 개의 공원을 관리할 수 있게 개발을 진행했던것 같습니다. 전체적으로 공원관리에 맞게 개발이 잘 이루어져 있으며, 웹을 통해서 사용자가 육안으로 쉽게 관리할 수 있을 것 같습니다. 그리고 시제 제작 데모를 이용하여, 각 기능을 잘 구현 하였으며, 데이터 수집 등을 잘 처리한 것 같습니다.
2. 작품 개요
2.1. 배경 및 목적
급격한 도시화, 인구 밀집, 자산 및 자원 낭비 등 도시화로 인해 발생하는 각종 문제에 대응하기 위해 도시 패러다임으로 ‘스마트 시티’가 등장하였다. 그중 행복의 질에 큰 영향을 미치는 도시 공간 중 하나인 공원은 ‘스마트 파크’로 진화하고 있다. 스마트 파크는 다양한 첨단 기술을 활용하여 이용자의 공원 체험 향상, 공원 운영·관리의 효율성을 높이며 도시가 직면한 사회 및 환경문제를 해결하는 지속 가능한 공원이다.
스마트 파크의 국내 사례로 대구에 도입된 ‘IoT See Park’는 4차산업혁명 핵심기술을 공원에 접목하여 인공지능 CCTV, 전역 무료 WiFi, 스마트 휴게시설(태양광 벤치), 스마트 방향 표지판 등의 서비스를 제공하고 있다. 또한 세종시 호수공원에는 체험존 AR(GPS 기반 방향 안내 앱), VR 체험관 등 AR/VR을 사용한 공원 체험 향상 관련 서비스를 제공한다. 이처럼 대부분의 스마트공원은 이용자 중심 공원 서비스 제공에 초점이 맞춰져 있다. 반면에 스마트공원 관리 측면에서는 서비스 구축이 미비하다. 이에 본 작품은 관리를 목적으로 한 IoT 중심의 지능형 스마트 공원관리시스템인 스마트파크를 구현하였다. 스마트파크는 공원 내 시설 및 자연생태환경에 대한 관리를 웹을 통해 손쉽게 할 수 있고, 공원관리에서 수집되는 정보를 바탕으로 데이터 시각화를 제공한다.
2.2. 오픈소스 IoT 플랫폼 개선 및 활용
스마트파크은 스마트 공원의 센서 데이터를 수집, 전처리, 관리, 시각화, 데이터에 따른 명령수행 등의 기능이 있다. 이를 구성하기 위해 본인이 참여했던 IoT 오픈소스를 스마트파크을 위한 기능 및 서버를 추가·개선하여 사용하였다.
데이터 수집
Kafka를 사용하여 스마트 공원의 센서 네트워크로부터 방대한 양의 센서 데이터를 수집할 수 있도록 하였다. 수집한 데이터를 전처리 서버로 보내기 위한 브로커 역할을 한다.
데이터 전처리
서버 프레임워크로 제작한 서버를 이용하여 데이터 전처리, 로직 검사 작업을 수행한다. 센서 데이터의 등록정보(공원이름, 서비스이름 등등) 추가 및 현재 데이터가 등록된 명령(액추에이터 동작, 이메일전송 등등)수행 조건을 만족하는지에 대한 검사하는 역할을 한다.
관리 및 시각화
전처리된 데이터를 NoSQL데이터 베이스인 ElasticSearch에 저장하여 관리한다. 수많은 센서 데이터값이 계속해서 추가되므로 Write 기능에 강점을 가진 데이터 베이스를 선택하였다. 또한, Kibana를 사용하여 ElasticSearch에 저장된 데이터들을 분석, 시각화할 수 있도록 하였다.
2.3. 스마트파크 IoT 플랫폼으로 관리하는 3가지 스마트 기기(센서노드)
본 프로젝트에서는 인체감지를 통해 보행량에 따른 밝기 조절이 가능한 스마트 가로등 관리, 쓰레기통 적재량 확인·알림, 센서를 이용한 수목 관리 등 IoT 중심의 지능형 스마트 공원관리시스템인 스마트파크(Park)를 구현하였다.
스마트파크에서 활용이 가능한 스마트 기기 총 세 가지를 제작했다.
구현한 세 가지 스마트기기는 모두 공통적으로 LoRa통신을 한다. LoRa는 LPWAN(저전력 장거리 무선통신)기술로, LoRa통신을 통해 저전력으로 광범위한 통신을 가능하게 했다. LoRa의 데이터 전송속도가 빠른 편은 아니지만, 공원 관리를 위해 이용되는 센서 데이터들은 실시간 처리보다는 아주 천천히 변화하는 센서 데이터들이며 장거리 통신이 요구되기 때문에 LoRa통신은 본 공원관리 시스템에 적합한 통신이다.
스마트 가로등
크게 세 가지 기능이 있다.
(a) 평균적으로 보행자 수가 많은 시간대에 밝기를 밝게 하고 적은 경우에는 밝기를 낮춤으로써 전력소비를 줄일 수 있다
(b) 자동 점소등 시스템이 가능하다. 가로등 상단 부분에 장착된 조도센서를 통해 어두워질 경우 자동으로 점등이 가능하다.
(c) 부점등 알림 기능이 있다. 가로등 하부(바닥) 부분에 부점등 확인을 위한 조도 센서가 장착되어 가로등이 켜져있는 밤에 조도센서(바닥 위치)에서 받은 값이 어두운 값이면 조명이 제대로 안 켜진 상태로 자동적으로 판단하여 관리자에게 알림을 줄 수 있다.
쓰레기 관리
스마트 쓰레기통 관리의 경우에는 쓰레기통의 적재량을 주기적으로 알 수 있으며, 로직 등록 기능을 통해 쓰레기가 많이 쌓이면 이메일과 같은 알림을 줄 수 있다. 쓰레기통에 쓰레기가 완전히 채워지기 전에 미리 알림을 주어 쓰레기 수거에 도움을 줄 수 있다. 따라서 공원의 더 나은 청결을 기대할 수 있다.
스마트 수목 관리
수목 관리의 경우, 토양센서를 통해 토양의 수분을 측정하고 수분이 부족하면 수중모터를 통해 물을 주도록 한다. 로직 등록 기능을 이용한 자동화로 공원의 수목 관리를 더 편리하게 할 수 있다는 장점이 있다.
3. 작품 설명
3.1. 주요 동작
스마트 쓰레기통
공원 내 비치된 쓰레기통의 적재량 정보 파악
쓰레기통에 쓰레기가 가득 차면 쓰레기통을 비워주기 위해 알림을 줄 수 있다.
쓰레기통의 적재량은 쓰레기통의 뚜껑에 부착된 초음파 센서를 통해 거리를 탐지하여 알 수 있다. 이를 통해 얻은 값은 LoRa통신을 통해 LoRa Gateway로 전송된다. LoRa Gateway는 네트워크 서버(싱크 노드)로 전달을 해주어 최종적으로는 백엔드 서버로 값이 전달된다.
적재량 모니터링 및 알람
쓰레기통의 적재량을 모니터링하며 일정 수준 이상의 쓰레기가 쌓이면 쓰레기가 넘치기 전에 쓰레기를 수거할 수 있도록 알림을 줄 수 있다. 사용자는 프론트엔드 웹에서 직접 조건을 추가하여 적재량이 몇 프로 이상일 경우에 이메일과 같은 알림을 주도록 설정이 가능하다.
데모
다음은 쓰레기통 전면이다.
위 사진은 뚜껑 내부에는 초음파 센서가 부착된 모습이다. 거리 측정으로 적재량을 판단한다.
위는 쓰레기통 적재량 변화값을 시각화한 그림이다. 이와 같이 각 공원 쓰레기통의 적재량을 한 눈에 확인할 수 있다.
쓰레기통 적재량이 90프로 이상일 경우, 이메일을 전송하도록 로직을 등록한 것이고 그림 6은 이메일 수신 화면이다.
마트 가로등
보행자 통행량 측정
시간별/요일별/계절별/지역별 유동인구 측정 및 분석을 하여 유동인구가 많은 가로등만 점등하여 에너지 절약할 수 있다. 결과적으로 유동인구에 따라 가로등의 밝기를 조절하여 에너지를 절약한다.
PIR 센서를 통한 통행량 측정
유동인구(보행자 통행량)은 가로등에 PIR 모션감지 센서를 부착하여 지속적으로 움직임을 감지한다. 모션 감지가 오랫동안 일어날수록 유동인구에 대한 누적값이 증가한다. 이와 같은 방법으로 센서노드에서 얻은 모션감지의 수치를 백엔드 서버까지 전달하게 된다.
데모
가로등 유동인구에 따른 밝기 조절
다음 사진과 같이 가로등의 상단 부분 LED 옆에 PIR 모션 감지 센서가 부착되어 있다. 모션 감지 센서에서 움직임이 감지되는 시간이 증가할수록 통행량에 대한 값이 증가하게 된다.
방문자 수 변화
위 그림은 방문자 수 데이터를 시각화한 자료이다. 직접 센서로 측정해 테스트하고 있는 sangdo의 방문자 수 변화를 볼 수 있다.
위는 보행량에 따라 led 밝기 조절 로직이다. 스마트파크(park)에서는 실질적인 데이터를 수집할 수 없기에 value 범위를 임의값으로 설정하였고, 실제로는 대시보드에 나타난 방문자 수를 참고하여 value 범위를 설정해야한다.
보행량에 따른 밝기 조절
위와 같이 유동인구에 따라 가로등 LED 밝기가 달라진다.
가로등 부점등 검사 (고장 알림)
가로등 하부의 조도 센서를 통해 가로등의 전구의 조도를 측정 후 LED ON이어야 하는 상태에서 조도가 측정되지 않으면 가로등이 부점등 상태임을 이메일로 알린다.
그림 14는 부점등 상태일 경우 이메일을 전송하도록 로직을 등록한 것이다. illuminance는 가로등 상단에 있는 조도센서(낮과 밤 판별)의 데이터이고, Lampilluminance는 가로등 하부에 위치한 조도센서(부점등 판별)의 데이터이다. 이때 조도센서의 값이 90 ~ 100 이하라면 어두운 것으로 판단한다. illuminance < 90 인 경우 LED가 ON되어야 하고, 이때 LED는 아래의 조도센서를 향해 빛을 쏘기 때문에 Lampilluminance는 LED 불빛을 감지해 100 이상이어야 한다. LED가 정상적으로 켜지지 않아 Lampilluminance의 값이 100 이하라면 부점등 상태로 판단한다. 따라서 0 < illuminance < 90 이고 0 < Lampilluminance < 100일 때 부점등 상태라 판단해 이메일을 보내도록 로직을 등록했다. 오른쪽 그림은 어두운 상태에서 LED가 정상적으로 켜졌으나, 가로등 하부 조도센서를 손으로 가려 부점등 상태를 조성한 것이다.
밝기에 따른 가로등 자동 점·소등
밝을 경우에는 로직에 등록된 LED 액추에이터 명령을 통해 LED가 자동으로 꺼지고, 어두울 경우 액추에이터 명령을 통해 자동으로 켜질 수 있도록 로직을 등록했다.
가로등 상단의 조도센서를 통해 어두워지거나 밝아질 경우 자동 점/소등한다.
두 번째 그림에서 볼 수 있듯이 밝을 경우 LED를 OFF하고, 세 번째 그림에서 조도센서를 가려 인위적으로 어두운 환경을 만들었을 때 LED가 ON되는 것을 확인할 수 있다.
스마트 수목 관리
온도, 습도, 토양 등 측정센서 설치 및 생장 환경 관련 데이터 측정한다. 공원 내의 수목주변의 온도와 습도, 일조량, 토양내 습도 등을 측정 분석하여 알릴 수 있다. 실외의 온도와 습도 일조량, 등을 컨트롤하기는 힘들어 보이므로 모니터링 및 알림 기능을 통해 관리를 도울 수 있다.
관수 기능 자동 제어
토양 센서를 이용해 습도를 측정하여 필요할 경우 관수 시설을 제어하는 기능을 갖고 있다.
토양 수분 센서를 이용하여 토양의 수분이 부족할 경우 액추에이터 명령을 통해 수중모터(펌프)를 작동시켜 토양에 물을 준다. 센서노드는 토양 수분 센서의 값을 일정 주기로 게이트웨이로 전송한다. 센서 데이터는 센서노드 -> 게이트웨이 -> 싱크노드 -> 백엔드 순으로 전달된다.
데모
첫 번째 그림에서 볼 수 있듯이 수중모터 제어를 위해 액추에이터로 pump를 등록했다. 토양수분 값이 일정 값 이상이면 사용자가 등록한 액추에이터 값에 따라 수중모터가 동작하도록 로직을 등록했다.
백엔드는 토양수분 센서가 일정 값 이상인지 조건을 확인하여 조건에 부합하면 액추에이터 신호를 싱크노드에 전달하여, 싱크노드->게이트웨이->센서노드 순서로 액추에이터 명령이 전달된다.
왼쪽 사진은 등록된 로직에 따라 사용자가 설정한 범위 내 습도값이 해당될 시 액추에이터를 작동시켜 수중모터로 물을 공급하는 모습이다. 오른쪽은 토양수분을 그래프로 시각화한 자료이다.
데이터 시각화
의미가 없거나 혹은 단순한 의미만을 지닌 데이터로부터 실시간으로 의미있는 정보를 찾는 것 자체가 가치를 창출하는 일이다. 스마트파크는 단순한 센서데이터 들을 이용해 여러 가지 의미 있는 정보를 대시보드에 시각화 하였다.
공원 검색기능
원하는 기간, 원하는 공원을 선택하면 선택된 데이터만을 필터링하여 볼 수 있다.
공원 별 센서값 시각화 보드
공원의 모든 센서값을 시각화한 보드를 통해 센서값의 비교 및 확인하여 공원 별 기상정보 비교분석이 가능하다.
공원 별 방문자 수 비교 분석 기능
공원 별 방문자 수 데이터 시각화를 통해 비교 분석이 가능하다. 각 공원 별 방문자 수를 한 눈에 알아볼 수 있고, 어느 시간대, 어느 요일에 유동량이 많은지를 쉽게 알 수 있어 이를 이용해 방문자 수에 따른 가로등 제어, 공원 내 행사 일정 선택 등 다양한 방면의 활용이 가능하다.
공원 별 쓰레기통 적재량 비교분석 기능
원하는 공원의 쓰레기통 적재량을 실시간으로 체크할 수 있으며, 요일 날짜별 쓰레기통 적재량을 한 눈에 볼 수 있어, 쓰레기통 추가 배치, 수거 등에 유용한 활용이 가능하다.
서비스 유지보수의 용이
IoT서비스 플랫폼에서 지속적으로 안정적인 서비스가 제공되기 위해서는 센서를 비롯한 하드웨어들의 헬스 체크가 필요하다. 스마트파크에서는 서비스 노드의 헬스 정보를 수시로 체크하여 웹 UI상에 나타내 주므로 헬스 정보 확인이 가능하며 문제 발생 시 빠른 유지보수가 가능하다.
유연한 확장성
플랫폼의 운영자가 스마트파크이 관리할 새로운 공원을 하거나, 새로운 IoT서비스를 추가할 경우 웹상에서 몇 가지 조작만으로 공원 및 서비스 등록이 가능해, 유연한 확장성을 보여준다.
3.2. 주요 특징
UI에서 IoT 디바이스 관리, 데이터 시각화
네비게이션 바
MANAGEMENT 탭의 구성요소로 Sensor, Node, Sink, Actuator가 있다. SERVICE 탭의 구성요소로 LogicCore가 있다. 사용자는 LogicCore에서 로직을 등록할 수 있다. Kibana 탭의 구성요소로 Dashboard가 있다. Dashboard에서 데이터 시각화를 제공한다.
등록(register)
센서 : register sensor 버튼을 클릭하면 그림 33과 같이 등록 페이지가 나타나고, 사용자는 센서 정보 (센서 이름, 종류, value)를 입력해 센서 등록을 할 수 있다.
싱크 : register sink를 클릭 후 싱크 정보 (싱크 이름, topic, ip:port)를 입력해 싱크를 등록할 수 있다. 스마트파크(park)에서는 싱크를 한강 공원 단위로 설정해 진행하였다.
노드 : 노드 등록은 그림 32와 사진과 같이 노드 이름, 위치 (지도를 클릭하면 해당 위치의 위도 경도를 얻음), 노드의 종류(가로등, 쓰레기통, 수목), 해당 노드에서 사용할 센서, 싱크를 선택해 등록할 수 있다.
이때 노드는 가로등, 쓰레기통, 수목을 의미한다.
액추에이터 : register actuator를 클릭 후 액추에이터 이름을 입력해 등록할 수 있다. 본 프로젝트에서는 토양 습도를 토대로 관수 제어를 하기 위한 pump 액추에이터와 조도에 따라 led 밝기를 조절하기 위한 led 액추에이터를 등록하였다.
각 정보를 입력후 submit 버튼을 클릭하면 센서, 싱크, 액추에이터, 노드의 정보를 서버로 POST한다.
table
센서, 싱크, 액추에이터 : 위와 같이 사용자가 등록한 센서, 싱크, 액추에이터의 정보를 table의 형태로 확인 가능하고, 휴지통 img를 클릭하여 삭제할 수 있다.
노드 : 노드는 table을 볼 수 있는 두 가지 옵션 (All, Map)이 있다.
· All 옵션 : default로 설정되어있는 노드 table이며 사용자는 싱크별 노드 table을 볼 수 있다. 스마트파크(park)에서는 한강 공원별 가로등, 쓰레기통, 수목 노드가 table로 나타나 있다.
· Map 옵션 : 마지막 사진과 같이 지도상에서 마커 형태로 노드 위치를 나타낸다. 각 노드의 종류가 마커 이미지로 구분되어 있고, 마커를 클릭하여 해당 노드의 정보를 볼 수 있다. 이때 사용자는 지도를 움직이며 마커 위치를 확인할 수 있다. 마커 이미지 색상은 가로등 : 파란색 / 쓰레기통 : 노란색 / 나무 : 초록색이다.
· 노드 헬스체크 : 사용자는 색상별로 노드의 상태를 확인할 수 있다. 웹 소켓으로 서버와 통신하기 때문에 실시간으로 확인이 가능하다.
· 노드의 상태
회색 : 알 수 없는 상태
초록 : 연결된 상태
노랑 : 직전에 연결이 끊긴 상태 (초록과 빨강의 사이)
빨강 : 연결이 끊긴 상태
로직코어 (LogicCore)
· Logic 등록 : 사용자는 LogicCore table에서 register logic 버튼을 클릭해 로직을 등록할 수 있다.
Logic 정보
· 로직 이름
· 센서 : 센서에 로직 등록
· 시간 범위 : 해당 로직이 작업을 수행할 시간 설정
· value 범위 : 해당 로직이 동작할 value 설정
· action : 사용자가 설정한 시간과, value 범위 내 센서값이 해당할 때 실행할 작업
◎ email : 해당 이메일로 알림 메시지 발송
◎ actuator : value, sleep 값을 받아 해당 액추에이터 작동
register logic에서 각 요소(시간, value, action)를 카드라 할 때 value 카드와 action 카드는 왼쪽 하단 Add ~ 버튼을 통해 추가할 수 있고, value 카드와 시간 카드는 Add scope 버튼을 통해 범위를 추가할 수 있다.
· Logic table : 아래 왼쪽 사진과 같이 등록한 logic을 table의 형태로 볼 수 있고, 휴지통 img를 클릭하여 삭제할 수 있다.
· Show logic : table에서 show logic 버튼을 클릭하면 아래 오른쪽 사진과 같이 로직의 구조를 볼 수 있다.
이처럼 스마트파크(park)은 웹에서 센서, 노드, 싱크, 액추에이터를 등록하고, 사용자 맞춤 로직을 생성할 수 있기에 스마트공원 관리에 용이하다.
kibana dashboard
home 또는 kibana dashboard 메뉴를 통해 시각화된 데이터 확인이 가능하다.
LPWAN 통신 방법인 LoRa 통신, 직접 구현한 LoRa WAN 프로토콜로 작동
날씨 상태, 토양 수분 레벨 또는 가로등과 같이 이러한 응용 분야에서 측정되거나 제어되는 크기/양은 모두 확장된 시간 범위에 걸쳐 매우 느리게 변화한다. 또한, 센서 노드가 서로 멀리 떨어져 있는 경우가 많고 배터리로 구동되는 경우가 많으므로, 최적의 무선 프로토콜은 최소한의 전력 소비로 긴 거리에 걸쳐 작은 데이터 패킷을 효율적으로 전송할 수 있어야 한다.
따라서 대표적인 LPWAN(저전력 무선 장거리 통신) 기술 중 비면허 대역 주파수를 이용하는 LoRa 통신을 선택했다. 1Ghz 이하의 대역은 다른 주파수에 비해 서로 간섭력이 낮아 무선 파장 및 효율적 측면에서 좋기 때문에 원거리 통신을 위해서 적합하다. End-Device(센서노드)와 LoRa 게이트웨이, 싱크노드를 구현하여 LoRaWAN 프로토콜을 따르도록 구현했다. End-Device의 LoRaWAN 프로토콜은 C언어로 구현하였으며, LoRa Gateway의 LoRaWAN은 Python으로 구현했다. LoRaWAN의 모든 Mac Command가 구현된 것은 아니지만 필요한 몇가지 Mac Command가 작동하도록 구현했다. DevStatusReq와 DevStatusAns의 Mac Command를 통해 센서노드의 배터리와 수신신호의 수치를 파악할 수 있다. 또한 RFU의 예약된 공간에는 ActuatorReq와 ActuatorAns의 Mac Command를 구현하여 이 명령을 이용하여 액추에이터 작동이 가능하도록 했다. Actuator는 백엔드 서버에서 오는 명령을 End-Device에서 처리하는 기능을 말한다. 예를 들면 백엔드에서 모터를 작동시키거나 LED의 밝기를 조절하는 등의 명령을 센서노드에 전달할 수 있다.
백엔드 서버 특징
5G 시대가 도래하면서 IoT(사물 인터넷) 관련 데이터의 가치가 더욱 커질 전망이다. IoT 기술의 특성상 수많은 데이터와 로그를 빠르게 적재, 분석하는 것이 필수적이다. 스마크파크는 Kafka – DataLogic서비스 – ElasticSearch로 데이터 파이프라인을 구성하여 방대한 센서 데이터의 준 실시간 처리가 가능하다.
3.3. 전체 시스템 구성
3.3.1. 프론트엔드
컴포넌트 구조
위 그림은 컴포넌트 구조를 간단히 map으로 표현한 것이다, App에서 모든 컴포넌트를 route한다.
· ‘~ Management’ : sensor, node, sink, actuator를 관리하는 컴포넌트이다. 각각의 resigter 컴포넌트를 호출해 정보를 등록하고, 각 table 컴포넌트에서 해당 list를 table 형태로 출력한다.
· Register Logic : 로직 등록 컴포넌트이다. 하위 컴포넌트 InputCards에서 사용자가 입력한 time, sensor, value, action, actuator card의 정보 넘겨주고 이를 바탕으로 로직을 등록한다.
· LogicTable : 등록된 로직을 list로 나타내는 컴포넌트이다.
· showLogic : logic table에서 show logic을 클릭할 시 호출되는 컴포넌트이다. 하위 컴포넌트 showCards에서 사용자가 등록한 정보를 가져와 해당 로직의 정보를 출력한다.
· kibanaDashboard : 데이터 시각화를 제공하는 컴포넌트이다.
3.3.2. 백엔드
데이터 파이프라인
개요
그림 48처럼 센서데이터가 라즈베리파이(Sink Node, 공원)에서 최종 저장소로 바로 전송되는 경우 라즈베리파이 내에서 측정값과 등록정보(metaData)를 모두 관리해야 하기 때문에 센서 네트워크에 매우 큰 부담을 준다. 또한 스마트파크의 매우 많은 라즈베리파이가 하나의 저장소로 데이터를 전송하는 경우 Elasticsearch에게 매우 높은 가용성과 처리량을 요구하게 된다.
따라서 그림 49와 같이 우리는 최종 저장소 이전에 데이터를 처리할 수 있는 DataLogic서비스를 추가하였다. 센서 네트워크에서는 측정값과 각 Node의 고유ID만 관리하고 DataLogic서비스에서 데이터 처리 서비스(데이터에 등록정보를 추가)를 지원한다. 해당 서버는 측정값을 이용해 등록된 이벤트를 처리하는 기능도 지원하는데, 이는 다음에 설명하겠다.
여러 공원의 데이터 값을 하나의 데이터 처리 서비스로 전송하는 것 또한 데이터 처리 서비스에 너무 많은 부담을 준다. 그렇다고 여러 개의 데이터 처리 서비스를 구현하기에는 구성이 너무 복잡해지며, 특정 서버에 요청이 몰릴 경우 몇몇 요청은 처리하지 못하고 누락될 수 있다.
따라서 센서네트워크와 서버 사이에 Kafka를 배치해주었다. Kafka는 데이터를 임시로 저장하는 Queue역할을 하며 기본적으로 클러스터 환경을 제공하여 높은 처리량과 고가용성을 보장한다. DataLogic서비스는 자신이 필요할 때만 Kafka에 데이터를 요청하기 때문에 과부하로 문제가 될 가능성이 배제된다.
구성
Kafka
producer – Broker(Kafka) – Consumer 구조의 스트림 처리 플랫폼이다. 여러 공원으로부터 데이터를 저장받아 임시저장한 후 DataLogic서비스에 안정적으로 데이터를 제공한다.
라즈베리파이에서 카프카로 전송되는 데이터의 예시는 오른쪽 그림과 같다.
DataLogic
서버 세부설명 파트에서 자세히 설명한다.
엘라스틱서치
방대한 센서데이터를 지속적으로 저장하기 위해서 Write능력이 뛰어난 데이터 베이스가 필요하다. 따라서 Write 효율에 중점을 둔 NoSQL데이터베이스인 Elasticsearch를 데이터 베이스로 사용하였다.
Elasticsearch에 준 실시간으로 저장된 센서 데이터는 비교/분석을 위한 시각화와 공공데이터 배포를 위해 사용된다.
Kibana
Paul Brunet의 InfoWorld 칼럼에 따르면 요즘 시대는 더 많은 데이터에 접속할 수 있는 상태지만, 이런 수많은 데이터에서 효과적으로 인사이트를 획득하는 능력은 감소했다고 한다. 이제 우리가 고민해야 할 지점은 ‘데이터를 어떻게 잘 활용할 것인가?’이다. 많은 양의 데이터를 한눈에 볼 수 있고, 데이터 분석에 대한 전문 지식이 없어도, 누구나 쉽게 데이터의 패턴을 알아낼 수 있도록 시각화된 데이터 대시보드를 구현하였다.
서버 상세설명
서버 아키텍쳐
클린 아키텍처를 적용하여 특정 계층에 대한 수정이 다른 계층에 거의 영향을 주지 않도록, 그리고 같은 계층 내에서는 일관되고 응집력 있는 결합을 제공할 수 있도록 서버의 계층을 구분하였다. 상당히 분량이 많은 앱이더라도 소스코드 전반을 쉽게 파악할 수 있고 복잡한 수정 사항이 생겼을 때라도, 어떤 부분들을 고치면 되는지 금방 파악할 수 있도록 하였다.
Application – Registration
개요
등록 정보를 관리하기 위한 API Server이다. 프론트엔드에서 등록한 메타데이터, 로직데이터 등을 백엔드의 데이터 베이스에 저장하고, 이 데이터 베이스를 읽어서 프론트엔드, DataLogic, HealthCheck 등의 서비스에 정보를 제공 및 컨트롤하는 역할을 수행한다.
구성
모든 서비스는 이용자가 어떤 기능을 요구하거나, 개발자가 어떤 아이디어를 생각해내 서비스를 확장시켜야 하는 상황이 생기기 마련이다. 이때를 대비해 유연하게 확장이 가능하도록 서버를 구성하여야 한다. 스마트파크의 모든 서버는 각 기능 단위로 서비스를 만들어 큰 서비스를 구성하는 방식인 마이크로 서비스 아키텍처로 구성하였다.
기능
스마트파크를 구성하는 마이크로 서비스들을 컨트롤하는 역할을 한다. Registration은 센서 데이터 스트림 처리를 담당하는 마이크로서비스(DataLogic)와 센서 데이터 스트림 노드(아두이노)의 헬스 상태 처리를 담당하는 마이크로서비스(HealthCheck)에게 등록정보를 제공한다. 이후 등록정보의 변경이 발생할 경우에도 마이크로 서비스 들에게 등록 정보를 제공해준다.
DB
스마트파크의 데이터베이스는 위 사진과 같이 스키마가 정의되어있다. 싱크 노드, 센서 노드 등의 메타데이터를 구조적으로 저장 및 조회한다. 데이터베이스로 쓰인 애플리케이션은 mySql이다.
LogicCore – DataLogic
DataLogic서비스는 Kafka에서 데이터를 읽어온 뒤 등록정보를 바탕으로 메타데이터를 추가한다. 보강된 데이터를 기반으로 사용자가 지정한 이벤트의 처리동작과 Elasticsearch로의 데이터 저장을 수행한다.
등록정보 추가
DataLogic서버는 처음 실행될 때 Registration 서버에 등록정보를 요청한다. Registration서버는 요청한 DataLogic서비스를 자신의 서버 내에 등록한 후 서버 내 DB에 저장된 Sink node와 Sensor node의 등록정보들을 반환해 주고 DataLogict는 이 등록정보를 서버의 메모리 내에 보관한다.
DataLogic서비스가 실행되고 있는 도중에 등록정보가 변경될 경우 Registration서버는 업데이트된 정보를 등록된 모든 DataLogic서비스에게 알려주어 등록정보를 동기화할 수 있도록 한다.
센서 데이터마다 적절한 메타데이터를 얻기 위해 DataLogic서비스에서 DB에 접근할 수 없기 때문에 등록정보를 메모리에 동기화시킨다. 이후 Kafka에서 읽어온 센서 데이터에 sensor-id, node-id에 해당하는 메타데이터를 추가한다.
이벤트 처리
수집한 센서 데이터에서 프론트엔드에서 유저가 미리 지정해둔 특정한 이벤트를 검사하기 위한 기능이다. JSON 형식의 센서 데이터의 특정 필드 값을 기준으로 조건을 검사하는 filter기능과 actuator동작신호 전송, Email전송 등 특정 이벤트를 수행하는 action기능으로 구성되어있다.
Elastic search저장
DataLogic서비스 내에서 수많은 (보강된)센서 데이터들이 생성된다. 이 데이터 하나당 Indexing(Write) API를 호출하면 네트워크에 과부하가 걸리므로 이를 방지하기 위해 데이터를 효율적으로 저장하여야 한다.
따라서 저장할 데이터를 적재하다가 특정 byte 이상이 쌓이면 한 번에 Bulk Indexing(배치 처리)하도록 구현하였다. 또한, Elastic search의 Sharding기능을 통해 자체적으로 쓰기 성능을 향상시켰다.
HealthCheck
그림 60과 같이 수많은 노드의 정상 작동여부와 배터리 잔량을 전송받아 각 노드의 헬스 상태를 결정한 후 헬스 상태가 변경된 노드의 헬스 정보와 배터리 잔량을 프론트엔드로 전송하는 역할을 수행한다.
먼저 라즈베리파이로부터 연결된 모든 아두이노의 상태 정보를 bool 형태로 전달받는다. 전달받은 상태정보를 이용해 헬스 정보를 업데이트한 후 그림 61과 같이 헬스 정보가 기록된 Map 자료구조에 저장한 후, 업데이트 내역이 있는 아두이노의 헬스정보를 추려내어 웹소켓을 통해 프론트엔드로 전송한다. 배터리 잔량은 받은 값을 그대로 전송한다.
헬스 정보 업데이트 기준은 전달받은 상태정보와 가장 최근의 상태를 위 사진과 같은 기준으로 비교해서 결정한다.
3.3.3. 도커(Docker)
플랫폼 서비스는 고객(서비스 운영자)이 플랫폼의 모든 실행 환경을 손쉽게 사용자의 환경으로 가져올 수 있어야 한다. 이를 위해 스마트파크의 모든 서버는 도커 컨테이너 위에서 실행가능하게 도커 이미지로 변환하여 배포한다. 도커 컨테이너(단일 컨트롤 호스트 상에서 여러개의 고립된 리눅스 시스템들을 실행하기 위한 운영 시스템 레벨 가상화 방법)를 이용하면 서버 환경을 재구축하는 부가적인 작업 없이 개발 당시의 환경(라이브러리, 설정값, 종속성 및 파일)을 로컬에서 애뮬레이션할 수 있다. 다시 말해 큰 수정 없이 어느 환경에서든 모든 서버가 작동되도록 배포할 수 있다.
3.3.4. 센서 네트워크(NodeMCU-Raspberry Pi-싱크노드)
백엔드에 데이터가 도달하기 전까지의 상세구조 그림
오픈소스 사용 (Flask와 MQTT통신 설명)
싱크노드-Flask 사용
싱크노드가 웹서버를 운영함으로써 서버로부터 HTTP요청을 받을 수 있다. 서버는 특정 싱크노드에 HTTP 요청을 하여 액추에이터 명령을 내리게 된다. 싱크노드(네트워크 서버)에서 웹서버를 운영하기 위해 Flask 오픈소스를 이용했다.
Paho MQTT Client 사용
IoT 통신은 경량화와 유연성을 갖추어야 한다. 여러 통신 방법 중 MQTT는 M2M, IoT를 위한 프로토콜로서, 최소한의 전력과 패킷량으로 통신하는 프로토콜로 IoT통신에 적합하다. MQTT 통신을 위해서는 Paho client 오픈소스를 이용했다. 따라서 우리는 게이트웨이와 싱크노드간에 Mosquitto 브로커와 MQTT 통신을 이용해서 경량화된 통신을 이용한다.
LoRaWAN 프로토콜 이용
Class A와 C
LoRaWAN 프로토콜에는 세 가지 A, B, C 클래스의 디바이스 유형이 있다.
센서 네트워크에서 끝단에 위치한 센서노드들의 클래스를 이 세 가지 A, B, C 유형 중 하나를 지정하여 사용하게 된다. 현재 구현하여 이용이 가능한 센서노드의 클래스는 A와 C 두가지다.
A Class 통신 방식은 LoRa Device와 LoRa Gateway 사이에서 LoRa Device가 Gateway에게 메시지를 전송한 이후, 잠시 동안 두 번에 걸쳐 메시지 수신이 허용되는 방식이다. 송신하기 전에는 송신과 수신 모두 동작을 하지 않다가 송신이 진행되면, 그 순간에 잠시 동안 두 번에 수신 윈도우를 허용할 기회를 제공한다. 즉 A Class는 수신을 위해서 송신이 진행되어야 하며 송신 위주의 서비스에 주로 사용된다. 상시전원을 사용하지 않고 배터리로 운영하는 경우에 사용되기 때문에 가장 적은 전력을 사용하는 Class이다.
C Class 통신 방식은 수신 가능 상태를 유지하기 위해 다른 Class에 비해 최소 지연시간을 갖는다. 송신 시간을 제외하고는 수신을 계속 대기하는 상태가 지속되므로 세 가지 Device Class 중에서 가장 많은 전력을 소비한다. 그 때문에 충분한 전력 공급이 가능한 상황에서 이용되는 Device Class이다. 대부분의 순간 수신상태를 유지하기 때문에 수신이 중요한 서비스(예를 들면 Actuator 기능 및 원격제어)에 이용된다.
OTAA 활성화 방법(Join과정 및 암호화 방법)
새로운 최종 장치 (LoRa 장치)가 LoRa 네트워크에 추가되려면 활성화 프로세스를 거쳐야 한다. LoRaWAN 프로토콜에는 ABP 통신과 OTAA통신 두가지 활성화 방법이 존재한다. 현재 OTAA모드를 기준으로 작동되도록 구현되어있다.
센서노드가 가입 요청 메시지를 보내 가입 절차를 시작한다. 참여 요청에 DevEUI, AppEUI 및 DevNonce가 포함된다. DevEUI 및 AppEUI는 각각 Global End Device 및 Application Identifier를 나타내며 IEEE 나타내며 EUI-64 주소 공간 형식을 따른다. 우리 프로젝트에서는 센서노드의 EUI 주소값은 Arduino 코드에 하드코딩하여 지정한다. Join 활성화 과정에서는 App Key를 이용하여 AES-128 암호화하며, NwkSKey와 App_SKey가 활성화 과정으로 인해 생성된 이후에는 이 둘 중 하나를 이용하여 메시지를 AES-128 암호화한다.
위 그림은 OTAA모드 활성화 프로세스 과정을 나타낸다. AppKey는 최종 장치와 서버간에 미리 공유되는 Key값이다.
왼쪽 그림은 Join Request 메시지의 구조이다. End Device는 다음의 요청을 보낸다.
Join Request 메시지는 암호화되지 않는다. Dev Nonce는 엔드 장치에서 랜덤하게 생성된 값으로 이전에 수신한 요청의 DevNonce 값과 중복되는 Dev Nonce의 요청은 받아들이지 않아 공격을 방지한다.
오른쪽 그림은 Join Accept 메시지의 구조이다. 메시지를 App Key로 AES-128 암호화한다.
메시지의 프레임 구조
구현한 MAC Command
LoRaWAN은 다음과 같은 Mac Command, Command ID를 갖도록 되어있다. DevStatusReq와 DevStatusAns 명령을 이용하여 End-Device의 배터리 수준과 SNR 수신 신호 상태 값을 얻을 수 있다.
우리 프로젝트에서는 RFU 영역인 CID 0x0E를 액추에이터 작동을 지시하는 명령(ActuatorReq, ActuatorAns)으로 따로 구현하여 사용하도록 했다.
Network Server(Sink Node) 동작 및 기능
데이터 전송
싱크노드는 여러 게이트웨이에서 mosquitto 브로커를 통해 mqtt통신으로 들어온 센서노드별 데이터를 취합하여, 이를 백엔드 측의 데이터 큐인 Kafka로 전송해주는 역할을 한다.
헬스체크를 위한 센서노드 상태체크 명령 전달
또한 센서노드들의 상태를 체크하는 것은 IoT플랫폼의 기능에 있어서 필수적이다.
싱크노드에서는 주기적으로 센서노드들에게 DevStatusReq요청을 보내도록 “command/downlink/ActuatorReq/<센서노드ID>”토픽으로 MQTT 메시지를 발행하여 게이트웨이에서 센서노드들에게 DevStatusReq Mac Command를 전달하도록 한다. DevStatusAns을 정상적으로 답장한 센서노드들의 상태를 “정상”으로 판단하고 이를 백엔드의 헬스체크 서버에 알려준다.
이러한 상태 체크 과정에서 배터리 상태와 SNR 수신 신호에 대한 정보를 알 수 있으며, 백엔드 서버에는 각 센서노드의 상태와 배터리 수준을 함께 알려준다.
액추에이터 명령 수신 및 게이트웨이로 전달
백엔드 서버로부터 오는 명령으로 LED 또는 모터들을 제어할 수 있어야 한다. 싱크노드에서는 백엔드로부터 오는 명령을 Flask 웹서버에 들어온 HTTP요청으로 수신한다.
Flask 웹서버로 수신한 HTTP 액추에이터 요청을 json 데이터와 함께 수신하면 싱크노드는 이를 “command/downlink/ActuatorReq/<센서노드ID>”토픽으로 MQTT 메시지를 발행한다. 센서노드 측에서는 명령을 받기 위해 “command/downlink/+/<센서노드 ID>”를 구독했기 때문에 MQTT프로토콜을 통해 액추에이터 명령을 받을 수 있다.
3.4. 개발 환경
3.4.1 개발언어 및 라이브러리
3.4.2. 포트 포워딩
4. 단계별 제작 과정
4.1. 센서네트워크 구축
4.1.1. 싱크노드 구현
싱크노드의 메인코드에서의 동작 순서이다.
1. Flask 웹서버 실행
2. Kafka Producer 생성
3. mqtt 클라이언트 생성
4. HealthCheck를 담당, 관리하는 클래스 생성
5. Actuator관련 동작을 담당하는 클래스 생성
6. MQTT Callback함수 등록 및 MQTT Client의 Loop 시작
7. HealthCheck를 위한 TCP 소켓 생성
8. 쓰레드 생성->쓰레드 핸들러로 HealthCheck를 담당하는 함수 등록
9. 헬스체크 쓰레드 시작
4.1.2. NodeMCU LoRa 연결
사용하는 LoRa 모듈(SX-1278)의 Pin 구성은 다음과 같다.
ANT에는 안테나를 연결한다. SCK는 SPI-Clock Input을 담당한다. MISO는 SPI-Data Out을 담당한다. MOSI는 SPI-Data In을 담당한다. NSS는 SPI-Chip Select를 담당한다.
LoRa 통신 모듈과 NodeMCU간의 연결은 다음의 표와 같다.
나머지 디지털 핀과 아날로그 핀들은 센서나 LED, 모터제어를 위해 사용한다.
4.1.3. NodeMCU
LoRaWAN 코딩
NodeMCU의 LoRaWAN은 https://git.antares.id/lorawan-loraid/arduino-loraid의 코드를 뼈대로 하여 개선하였다. (지원가능한 주파수 채널 확장, MacCommand처리기능 추가, 수신이 되지 않는 문제 수정.)
다음은 LoRaWAN통신 코드이다.
#include “ToIoTwithLoRaWAN.h”
#include “config.h”
const sRFM_pins RFM_pins = {
.CS = 10,
.RST = 9,
.DIO0 = 2,
.DIO1 = 3,
.DIO2 = -1,
.DIO5 = -1,
};ToIoTwithLoRaWAN t;
double value = 0.0;
void setup() {
t.setupToIoTwithLoRaWAN(nodeId, interval, 0);
// LoRaWAN //LoRaWAN Setting
/*~~~중략~~~*/
// Join procedure
bool isJoined;
do {
Serial.println(“Joining…”);
isJoined = lora.join();
//wait for 3s to try again
delay(3000);
}while(!isJoined);
Serial.println(“Joined to network”);
}
void loop() {
t.pub(“sensor-uuid-1″, 1, value);
t.rcv();
wdt_reset();
}
현재 타이머 시간과 과거에 측정한 타이머 시간이 interval보다 크다는 것은 interval만큼 타이머 시간이 흘렀다는 것을 의미한다. 따라서 interval만큼의 시간을 주기로 하여 메시지를 송신하는 것이다.
QOS 변수는 사용자가 설정하는 값으로 1이상일 때, ACK를 수신해야지만 다음 새로운 메시지를 전송하도록 한다. QOS==0 이라면, ACK 수신여부와 관계없이 새로운 메시지를 계속 전송한다.
앞으로 센서노드는 위 LoRaWAN 통신코드를 베이스로 한다. 센서, 액추에이터에 대한 코드를 이 코드에 추가하여 가로등, 수목관리, 쓰레기통을 구현한다.
4.1.4. 싱글채널 게이트웨이 제작과정
기존의 LoraWAN 게이트웨이를 국내에서 접하기는 어렵게 되어 있어서 접하기 쉽도록 라즈베리 파이를 이용한 싱글채널 게이트웨이를 직접 구현하기로 했다.
LoRa 통신을 가능하게 해주는 LoRa 통신 모듈을 연결하기 위해 왼쪽 사진과 같이 납땜 작업을 했다. 오른쪽 사진과 같이 안테나를 통신 모듈에 연결하고 각 핀을 라즈베리파이에 연결했다.
Lora 모듈과 라즈베리파이는 아래와 같이 연결했다.
4.1.5. LoRa 게이트웨이(라즈베리파이) LoRaWAN 코딩
게이트웨이의 LoRaWAN 프로토콜을 다음과 같이 구조화하여 파이썬으로 구현했다.
LoRaWAN 프로토콜 전체적인 코드 구성
다음은 Python으로 구현한 LoRaWAN프로토콜의 라이브러리 구조이다.
· AES_CMAC.py: AES-128 암호화를 위한 함수들을 정의했다.
· CID.py: Mac Command들의 Command ID를 정의했다. MacCommand처리를 위한 함수들을 정의했다.
· Channel.py: 사용할 주파수 채널을 선택할 수 있도록 채널에 대한 정보들을 정의했다.
· DataPayload.py: 데이터 페이로드(FRM Payload)를 처리하기 위한 클래스와 메소드를 정의했다.
· Direction.py: Mac Header(MHDR)의 각 메시지들이 어떤 방향으로 향하는 메시지인지 정의하는 클래스와 메소드이다. (UP: 센서노드->게이트웨이 / DOWN: 게이트웨이->센서노드)
· FHDR.py: Frame Header에 대한 클래스와 메소드를 정의한다.
· JoinAcceptPayload.py: JoinAccept에 대한 메시지를 생성하거나 읽기 위한 Payload 처리코드
· JoinRequestPayload.py:JoinRequest에 대한 메시지를 생성하거나 읽기 위한 Payload처리코드
· LoRaMAC.py: 작업 상태를 정의하는 코드. (수신상태, 송신상태, 대기상태)
· MHDR.py: Mac Header 정보들을 정의하는 클래스와 메소드
· MacCommandPayload.py: MacCommand를 Frame Payload에 넣어서 전달하기 위한 코드
· MacPayload.py: Mac Payload를 생성하거나 읽기 위한 코드
· MalformedPacketException: 패킷 예외 처리에 대한 코드
· PhyPayload.py: Physical Payload를 생성하거나 읽기 위한 코드
LoRa WAN 프로토콜 코드 주요동작 순서
일반적인 데이터 수신과 송신의 Payload 처리는 다음과 같은 순서로 동작한다.
PhyPayload.py->MacPayload.py->DataPayload.py (주요 코드 순서, 다른 코드는 생략) : 전원이 켜진 센서노드는 네트워크에 접속하기 위해 주기적으로 JoinRequest 신호를 전송한다.
JoinRequest를 수신하게 되어 읽는 경우에는 다음과 같은 순서로 동작한다. : PhyPayload.py->MacPayload.py->JoinRequestPayload.py
JoinAccept를 송신하는 경우에는 다음과 같은 순서로 동작한다. : PhyPayload.py->MacPayload.py->JoinAcceptPayload.py
MacPayload를 송신하는 경우에는 다음과 같은 순서로 동작한다. : PhyPayload.py->MacPayload.py->MacCommandPayload.py
센서노드(NodeMCU)와 게이트웨이간 LoRa통신 확인
위와 같이 센서노드와는 LoRaWAN통신을 하고 싱크노드와는 MQTT 통신을 한다.
4.1.6. 헬스체크 구현
헬스체크는 센서노드의 상태를 체크하는 기능이다. 싱크노드(네트워크 서버)는 LoRaWAN 프로토콜의 Mac Command인 DevStatusReq를 센서노드로 주기적으로 전송하게 한다. 이를 주기적으로 처리하기 위해 HealthCheck 담당 쓰레드를 하나 만들어서 주기적으로 게이트웨이에게 명령을 전달해주고 TCP소켓을 열어 백엔드 측의 헬스체크 서버와 통신하도록 했다. 센서노드로부터 DevStatusAns의 응답을 정상적으로 수신하면 센서노드의 상태를 정상으로 판단하고 그렇지 않으면 불량으로 판단한다.
4.1.7. 액추에이터 구현
센서노드가 액추에이터 명령인 ActuatorReq를 수신하면 받은 액추에이터 ID를 타켓으로 동작시켜야 한다. 액추에이터 명령 패킷은 다음과 같은 Payload를 갖는다.
명령 메시지 구조 : Actuator ID, Sleep, Value, Sleep, Value, …
Actuator ID는 동작시킬 액추에이터의 ID를 의미한다.(각각의 액추에이터는 자신의 ID를 갖는다.)
Sleep은 액추에이터에 값을 넣어 실행시키기까지의 대기시간을 의미한다.
Value는 액추에이터를 동작시키기 위한 값을 의미한다.
아래는 액추에이터를 관리하기 위한 Actuator 구조체 정의부이다.
struct Actuator{
int actuatorId;
int value[6];
unsigned long interval[6];
unsigned long previousMillis= 0;
short int running_index=0;
bool run= false;
short values_len= 0;
};
위에서 받은 액추에이터 명령 메시지를 위와 같은 구조체에 입력하여 관리하게 된다.
Void Loop(){
If(millis() – previousMillis > interval)
{
PreviousMillis = millis();
~~~~~~~~~~
}
}
액추에이터에서 delay가 발생하더라도 LoRa통신에는 영향이 없도록 하기 위해 다음과 같이 루프문과 타이머를 이용하는 구조로 코드를 작성했다. 위의 코드에서 interval은 대기시간이다. previousMillis는 전에 저장한 타이머 시간을 의미한다. Millis() 함수는 현재 타이머 시간을 의미한다.
즉, 위의 코드는 타이머 시간을 측정하여 interval에 저장된 시간만큼 지났을 때, if문 내부의 코드가 실행되는 구조이다. 액추에이터 동작 함수에도 LoRa에 지장이 가지 않도록 하기 위해 위의 구조를 적용했다.
액추에이터의 경우 각자의 구조체 안에 자신만의 대기시간을 저장하는 interval 변수를 갖도록 하여 개별적으로 동작할 수 있다.
4.1.8. 가로등 제작 과정
먼저 가로등의 뼈대가 되는 모형에 4×2 LED를 부착하고 LED의 밝기를 조절했다. LED의 밝기는 analogWrite()함수로 아날로그 신호를 주면 조절할 수 있다. 위 사진과 같이 밝기 조절을 테스트했다.
다음으로 보행자 통행량을 측정하기 위한 PIR모션센서를 테스트했다. PIR 센서를 다음과 같이 LED 옆에 부착하고 NodeMCU와 연결하여 테스트했다.
일정 간격의 시간동안 모션이 감지되면 위와 같이 Welcome!이 출력된다.
보행자 통행량 측정에 이 PIR 센서를 활용하기 위해서 주기적으로 이 모션이 감지되는 횟수가 얼마나 많은지 카운트를 누적하여 누적한 카운트 값을 백엔드 서버에 전해준다.
위는 바닥과 상단에 부착된 조도센서 사진이다. 왼쪽 사진은 부점등 확인을 위해 조도 센서를 가로등 바닥부분에 부착하였다. 기판에 저항과 cds센서를 납땜하여 연결했다. 오른쪽 사진은 밝기를 측정하기 위해 상단에 조도 센서를 부착하였다. 상단의 조도센서는 밤에 자동 점등, 아침에 자동 불을 끄기위해 달았다.
아날로그 핀에 연결하여 조도 센서값을 확인했다.
다음은 가로등 센서노드의 메인 코드이다.
#include “ToIoTwithLoRaWAN.h”
#include “config.h”
/*~~~중략~~~*/
ToIoTwithLoRaWAN t;
double value = 0.0;
// Actuator 2
struct Actuator a2;
Servo myservo;/
/ PIR, CdS, LED 관련 핀 제어변수 선언
/*~~~중략~~~*/
void setup() {
pinMode(pirPin, INPUT);
pinMode(cdsPin1, INPUT);
pinMode(cdsPin2, INPUT);
pinMode(ledPin, OUTPUT);
t.setupToIoTwithLoRaWAN(nodeId, interval, 0);
// LoRaWAN Setting
/*~~~중략~~~*/
// Actuator setting a2.actuatorId = 2;
// LED actuator
// Join procedure bool isJoined;
do {
Serial.println(“Joining…”);
isJoined = lora.join();
//wait for 3s to try again
delay(3000); }
while(!isJoined);
Serial.println(“Joined to network”);
}
void loop() {
cdsVal1 = analogRead(cdsPin1);
cdsVal2 = analogRead(cdsPin2);
pirVal = digitalRead(pirPin);
if (pirVal == HIGH) {
if (pirState == LOW){
Serial.println(“Welcome!”); // 시리얼 모니터 출력
pirCnt=pirCnt+1.0;
pirState = HIGH;
}
}
else {
if (pirState == HIGH){
Serial.println(“Good Bye~”); // 시리얼 모니터 출력
pirState = LOW;
}
}
t.pub(“5,6,9″, 3, cdsVal1, pirCnt, cdsVal2);
t.rcv();
t.set_target_actuator(&a2);
t.actuator_LED(&a2, ledPin);
wdt_reset();
}
세 가지 부품(LED, PIR센서, 조도센서)과 LoRa SX1278 모듈을 NodeMCU와 연결하고 위의 코드로 데이터가 잘 전달되는지 확인한다. LED 밝기 조절을 위해 LED를 액추에이터로 등록하여 액추에이터 ID를 2번으로 설정해주었다.
4.1.9. 수목관리 시스템 제작 과정
먼저 시연을 위해 다음의 화분을 준비하였다. 토양수분을 측정하기 위한 센서를 NodeMCU보드와 연결이 편하도록 다음과 같이 기판에 부착하였다. NodeMCU는 LoRa통신 모듈과 연결돼있다.
물을 주기 위해서는 수중모터와 수중모터를 제어하는 L9110s 모터드라이버를 사용했고, 수중모터를 LoRa통신으로 제어할 수 있도록 액추에이터로 등록했다. 코드 부분에서는 수중모터를 제어하기 위한 Actuator 구조체를 선언했다.
액추에이터 신호가 왔을때 수중모터가 작동하여 물을 주는 것을 확인했다. 다음은 수목관리 센서노드의 코드이다.
#include “ToIoTwithLoRaWAN.h“
#include “config.h”
/*~~~중략~~~*/
int IN1=D3; // A-IA연결 A
int IN2=D1; // A-IB연결 /A
int pin_A0 = A0;
ToIoTwithLoRaWAN t;
float value = 0.0;
// Actuator Struct to use motor
struct Actuator a1;
void setup() {
t.setupToIoTwithLoRaWAN(nodeId, interval, 0);
pinMode(pin_A0, INPUT);
// Actuator Setting
pinMode(IN1,OUTPUT);
pinMode(IN2,OUTPUT);
digitalWrite(IN1,LOW);
digitalWrite(IN2,LOW);
a1.actuatorId = 1;
for(int i=0; i < a1.values_len; i++)
a1.value[i] = 0;
// LoRaWAN Setting
/*~~~중략~~~*/
// Join procedure
bool isJoined;
do {
Serial.println(F(“Joining…”));
isJoined = lora.join();
Serial.println(isJoined);
//wait for 3s to try again
delay(3000);
}while(!isJoined);
delay(1);
Serial.println(F(“Joined to network”));
}
void loop() {
value = analogRead(A0);
t.pub(“7″, 1, value);
t.rcv();
t.set_target_actuator(&a1);
t.actuator_L9110(&a1, IN1, IN2);
wdt_reset();
}
4.1.10. 쓰레기통 제작 과정
다음과 같이 뚜껑에는 초음파 센서를 접착제를 이용해서 부착하였다.
Echo Pin은 디지털 핀 D1에 연결, Trig Pin은 디지털 핀 D3에 연결했다.
다음은 쓰레기통 센서노드의 메인코드이다. 초음파 센서로 측정한 적재량 백분율 계산, LoRa통신 기능을 한다.
#include “ToIoTwithLoRaWAN.h”
#include “config.h”
/*~~~중략~~~*/
int trigPin = D3;
int echoPin = D1;
ToIoTwithLoRaWAN t;
void setup() {
t.setupToIoTwithLoRaWAN(nodeId, interval, 0);
pinMode(trigPin, OUTPUT);
pinMode(echoPin, INPUT);
// LoRaWAN Setting
/*~~~중략~~~*/
// Join procedure
bool isJoined;
do {
Serial.println(“Joining…”);
isJoined = lora.join();
Serial.println(isJoined);
//wait for 3s to try again
delay(3000);
}while(!isJoined);
delay(1);
Serial.println(“Joined to network”);
}
double MyPreviousMillis;
double MyInterval = 10;
double distance;
double MAX_distance=18.0;
void ultrasound() {
float duration;
if(millis() – MyPreviousMillis > MyInterval) {
digitalWrite(trigPin, LOW);
delayMicroseconds(2);
digitalWrite(trigPin, HIGH);
delayMicroseconds(10);
digitalWrite(trigPin, LOW);
// echoPin 이 HIGH를 유지한 시간을 저장 한다.
duration = pulseIn(echoPin, HIGH);
// HIGH 였을 때 시간(초음파가 보냈다가 다시 들어온 시간)을 가지고 거리를 계산 한다.
// 340은 초당 초음파(소리)의 속도, 10000은 밀리세컨드를 세컨드로, 왕복거리이므로 2로 나눠준다.
distance = ((double)(340 * duration) / 10000) / 2;
MyPreviousMillis = millis();
}
}
double value;
void loop() {
ultrasound();
value = ((MAX_distance – distance)/MAX_distance)*100;
t.pub(“8″, 1, value);
t.rcv();
wdt_reset();
}
수직으로 쓰레기통 바닥을 향해 거리를 측정하여 값이 작아질수록 쓰레기가 쌓인 것이다.
하지만 이 거리값을 cm단위로 그대로 전달하지 않고 적재된 양을 100분율로 계산하여 서버에 전달한다. 계산은 다음과 같다. (쓰레기 적재량 데이터의 단위는 %.)
Value = (쓰레기통 바닥까지의 거리 – 측정된 거리) / (쓰레기통 바닥까지의 거리) * 100
4.2. 데이터 스트리밍 시스템 구축
4.2.1. Data Logic 서비스
Kafka에 적재된 데이터를 DataLogic서비스로 consume후 Unmarshal하여 DataLogic내의 구조체에 저장할 수 있도록 consumer를 생성한다.
func NewKafkaConsumer() *group {
//—중략—
// kafkaConsumer 설정
cfg := sarama.NewConfig()
cfg.Version = sarama.V0_10_2_0
cfg.Consumer.Offsets.Initial = sarama.OffsetNewest
// kafkaConsumer 생성
kafkaConsumer.client, err = sarama.NewConsumerGroup([]string{setting.Kafkasetting.Broker}, setting.Kafkasetting.GroupID, cfg)
if err != nil {
panic(err)
}
// —중략—
go func() {
for {
// kafka로부터 데이터 Consume
= err = kafkaConsumer.client.Consume(ctx, setting.Kafkasetting.Topics, &consumer)
if err != nil {
log.Panicf(“Error from consumer: %v”, err)
}
if ctx.Err() != nil {
return
}
consumer.ready = make(chan bool)
}
}()
return kafkaConsumer
}
메모리에 저장되어있는 등록정보를 데이터에 추가 할 수 있도록 한다.
func (lcuc *logicCoreUsecase) ToLogicData(kd *model.KafkaData) (model.LogicData, error) {
n, err := lcuc.rr.FindNode(kd.NodeID) // NodeID를 key로 검색하여 해당 Node의 메타데이터를 가져옴
//–중략–
s, err := lcuc.rr.FindSensor(kd.SensorID) // SensorID를 key로 검색하여 해당 Sensor의 메타데이터를 가져옴
//–중략–
vl := map[string]float64{}
for i, v := range s.SensorValues {
vl[v] = kd.Values[i] // Kafaka로 읽은 센서 데이터 값 map에 넣음
}
return model.LogicData{ // 등록정보 추가된 센서 데이터 return
SensorID: kd.SensorID,
SensorName: s.Name,
Values: vl,
Node: *n,
Timestamp: kd.Timestamp,
}, nil
}
해당 센서값이 설정된 이벤트 조건을 만족하는지 검사한다. 로직을 만족할 경우 이벤트를 실행한다.
func (ve *ValueElement) Exec(d *model.LogicData) {
// –중략–
for _, rg := range ve.Range {
if rg.Min <= v && v < rg.Max { //value가 등록한 조건을 충족하는지 검사
isRange = true
}
}
if isRange {
ve.BaseElement.Exec(d) //충족할 경우 다음으로 등록된 Filter혹은 Action 수행
}
}
func (ae *ActuatorElement) Exec(d *model.LogicData) {
// –중략–
if ok {
ae.Interval[d.Node.Name] = false
go func() {
res := Actuator{ // Actuator에게 전할 명령 객체 생성
Nid: d.Node.Nid,
Aid: ae.Aid,
Values: ae.Values,
}
pbytes, _ := json.Marshal(res)
buff := bytes.NewBuffer(pbytes) // 명령을 buffrer에 담음
addr := (*adapter.AddrMap)[d.Node.Sid] // 등록정보로 부터 해당 노드의 Sink주소 가져옴
// Sink 라즈베리파이의 Flask 서버로 동작명령 Post
resp, err := http.Post(“http://”+addr.Addr+”/actuator”, “application/json”, buff)
if err != nil {
panic(err)
}
defer resp.Body.Close()
}()
// –중략–
}
}
등록정보가 포함되어 완성된 데이터를 엘라스틱 서치에 인덱싱 한다.
func (ec *client) insertDoc(d *model.Document) {
ec.docBuf = append(ec.docBuf, d) // 버퍼에 데이터 적재
if len(ec.docBuf) >= (ec.bufSize – 10) { // 데이터 개수가 일정 수준에 달하면
ec.bulk() // 벌크 인덱싱 실행
}
}
4.2.2. Health Check 서비스
각 Sink로부터 헬스 정보를 받는다.
받은 헬스 정보 중 변경 사항이 있는 노드의 헬스 정보만을 버퍼에 추가하여 웹소켓을 통해 front-end로 보낼 수 있도록 한다.
버퍼에 담긴 헬스 정보를 프론트 엔드로 전송한다.
// 어답더 계층의 NodeState상태와 메모리 계층의 statusRepo의 status table을 동기화시켜 주는 것
func (sr *statusRepo) updateNodeStatus(sinkID int, ns []adapter.NodeState, t time.Time) []model.NodeStatus {
// –중략–
// update the status checked from the sink node
for _, v := range ns { // v는 NodeSate 배열 중 한 원소
nsTable[v.NodeID] = true
nodeState, ok := sr.table[sinkID][v.NodeID]
// if new nodeState, regist new state
if !ok {
tempState := model.NewStatus(v.State, t)
sr.table[sinkID][v.NodeID] = tempState
res = append(res, model.NodeStatus{NodeID: v.NodeID, State: tempState.State, Battery: v.Battery})
continue
}
if isChanged := nodeState.UpdateState(v.State, t); isChanged { // 헬스정보가 변경되었을 경우 res배열에 추가
res = append(res, model.NodeStatus{NodeID: v.NodeID, State: nodeState.State, Battery: v.Battery})
}
sr.table[sinkID][v.NodeID] = nodeState // 상태 테이블(map)에 상태 저장
}
// –중략–
return res
}
4.3. 웹 프론트 환경 구축
4.3.1 노드 마커
addMarker(position: any, map: any) {
if ( position.kind === ‘streetlemp’) var imageSrc = ‘https://user-images.githubusercontent.com/50009240/111455569-4edede80-8759-11eb-952f-2df28fdcdc4c.png’
else if ( position.kind === ‘trashcan’) var imageSrc = ‘https://user-images.githubusercontent.com/50009240/111451567-e857c180-8754-11eb-9299-0ea55e430b17.png’
else var imageSrc = ‘https://user-images.githubusercontent.com/50009240/111451575-ebeb4880-8754-11eb-8459-eed8f0983c55.png’
var imageSize = new window.kakao.maps.Size(30 , 35),
markerImage = new window.kakao.maps.MarkerImage(imageSrc, imageSize), // 마커이미지 생성
marker = new window.kakao.maps.Marker({
map: map,
position: position.latlng, // 마커의 위치
image: markerImage // 마커 이미지
});
return marker;
}
카카오맵 api를 사용해 지도 및 지도 위 마커를 나타냈다. addMarker 함수의 파라미터로 받은 position는 노드 이름, 종류, 정보(싱크, 센서, id), 지도 내 좌표를 포함하고 있다. 따라서 노드의 종류에 따라 마커 이미지를 다르게 했고, 해당 노드의 지도 내 좌표에 마커를 생성했다.
4.3.2. rest api
componentDidMount() {
this.getsensorList(); // 센서리스트 GET하는 함수
this.getsinkList(); // 싱크리스트 GET하는 함수
}
// Get sensor list from backend
getsensorList() {
var url = SENSOR_URL;
fetch(url)
.then((res) => res.json())
.then((data) => {
this.setState({ sensorList: data }); // 얻은 데이터를 sensorList에 저장
})
.catch((error) => console.error(‘Error:’, error));
}
위는 백으로부터 센서 리스트를 GET해오는 코드이다. component DidMount는 컴포넌트가 화면에 나타나게 됐을 때 호출된다. 여기서 fetch()를 통해 해당 url로부터 노드 리스트, 센서 리스트 등의 정보를 GET한다. 백으로부터 얻은 데이터는 해당 구조체에 저장해 사용한다. 위 코드와 같이 REST API를 통해 백으로부터 정보를 얻어오고, 센서, 노드 등을 등록할 시 REST API (POST)를 사용해 백으로 정보를 보냈다.
4.3.3. 헬스 체크
export interface nodeHealthCheckElem {
nid: number;
state: number;
battery: number;
}
import { w3cwebsocket as W3CWebSocket } from ‘websocket’;
const client = new W3CWebSocket(HEALTHCHECK_URL) // 웹소켓 오브젝트 생성
componentWillMount() {
client.onopen = () => { // 웹소켓 연결 확인
console.log(‘WebSocket Client for Health Check Connected’);
};
client.onmessage = (message: any) => {//서버로부터 수신된 메시지 전달
console.log(message);
this.setState({
nodeState: JSON.parse(message.data),
// 헬스정보 nodeState에 저장
});
this.getHealthStateMap(); // health map update
this.getBatteryStateMap(); // battery map update
};
}
위는 웹소켓을 통해 헬스와 배터리 상태를 얻어오는 코드이다. ‘const client = new W3CWebSocket(HEALTHCHECK_URL);’ 를 통해 새 웹소켓 오브젝트를 생성하고, onopen 핸들러로 연결이 수립되었는지 확인한다. WebSockets는 event-driven API로써 메시지가 수신되면 “message” 이벤트가 onmessage 함수로 전달되게 된다. 따라서 백으로부터 받은 노드 id, health, battery 정보가 onmessage 함수로 전달되고, 이를 nodeState에 저장한다. 그 후 getHealthStateMap(), getBatteryStateMap() 함수에서 nodeState를 바탕으로 각각 health, battery 정보가 저장되어 있는 map을 업데이트 한다. 각 map에 저장된 health, battery 정보를 토대로 nodeTable, MapNodeTable에서 상태를 나타낸다.
5. 회로도
6. 임시 데이터 프로듀싱 서버
6.1. 기능
실제로 모든 공원에 센서를 설치하여 데이터를 얻는 것은 매우 어려우므로 실제 공공 통계 데이터를 처리 및 프로듀싱 하여 데이터 실시간 스트리밍 및 시각화, 매니지먼트 기능의 데모를 구현한다.
6.2. OPEN DATA
서울 열린 데이터 광장
· 스마트서울 도시데이터 센서(S-DoT) 환경정보
· 미세먼지, 온도, 습도, 조도
· 서울시 한강공원 이용객 현황 통계
· 월별 한강공원 이용객
기상자료개방포털
· 농업기상관측 정보
· 토양수분
7. 참고문헌 및 출처
· 카카오맵 api : https://apis.map.kakao.com/web/sample/multipleMarkerEvent2/
· 서울 열린 데이터 광장 – 스마트서울 도시데이터 센서(S-DoT) 환경정보 https://opengov.seoul.go.kr/data/20303041
· 서울 열린 데이터 광장 – 서울시 한강공원 이용객 현황 통계 https://data.seoul.go.kr/dataList/10798/S/2/datasetView.do
· 기상자료개방포털 – 농업기상관측(AAOS) https://data.kma.go.kr/data/grnd/selectAgrRltmList.do?pgmNo=72
· 대구 스마트공원 기사 : http://news.khan.co.kr/kh_news/khan_art_view.html?art_id=201709151701001
· 세종 스마트공원 기사 :https://www.korea.kr/news/visualNewsView.do?newsId=148850085
· LoRaWAN v1.0.4 Alliance-Specification 문서 : https://lora-alliance.org/resource_hub/lorawan-104-specification-package/
· LoRaWAN Alliance-Specification 문서 : https://lora-alliance.org/wp-content/uploads/2020/11/lorawantm_specification_-v1.1.pdf
· LoRaWAN Alliance-What-is-lorawan 문서 : https://lora-alliance.org/wp-content/uploads/2020/11/what-is-lorawan.pdf