아두이노 NodeMCU와 GPS센서 VK16E로 만들어 보는 TinyGPS 네비게이션 시스템
GPS 센서를 사용하여 간단하게 네비게이션을 만들어 보도록 하자. GPS 센서로는 시중에서 2 만 원 대에 흔하게 구입할 수 있는 VK-16E를 사용하기로 한다.
VK-16E 커넥터는 6줄 배선이 있다. 이 중 4가지 즉 전원(3V 또는 5V), Rx, Tx, 접지(GND) 배선만 사용하도록 한다.
아두이노 우노 기준으로 볼 때에 Rx, Tx 배선 핀에 해당하는 0,1 번 핀들이 있으나 이 배선들은 USB-2-Serial 통신에 사용하는 것이 원칙이므로 사용해서는 안 된다.
설사 프로그램을 업 로딩한 후라도 LCD 디스플레이나 OLED 디스플레이를 별도로 사용하지 않고 시리얼 모니터를 사용하는 경우라면 절대 사용해서는 안 된다. 따라서 SoftwareSerial.h 라이브러리의 지원을 받아 0,1번 핀이 아닌 예를 들자면 5,3번 핀을 소프트웨어 상 시리얼 통신선으로 사용하는 것이 좋겠다.
그밖에 VK16E 사용자 매뉴얼에 따르면 이 GPS 센서는 스타트 시간이 평균 39초 이상 소요
되는 것으로 되어 있으며 자체적으로 실시간 시계를 포함하는 것으로 되어 있으나 GPS 데이터 자체에 시간 데이터가 포함되어 있어 이러한 기능은 그다지 필요치 않은 기능으로 보인다.
일단 GPS 센서가 아두이노에 연결이 되어 충분한 스타트 시간이 충분히 경과하면 녹색 LED가 블링킹하게 되는데 이때부터 네비게이션 프로그램이 실행 가능해진다.
프로그램을 업 로딩 후 야외에서 시험해 보기 위해 투명 플라스틱에 NodeMCU 보드와 리튬 배터리를 넣고 외부에 GPS 센서를 부착하였다.
프로그램 구조는 아두이노 NodeMCU와 디스플레이로 활용하기 위한 스마트폰과의 와이파이 연결 설정이 우선적이다. 그러한 프로그램 구조에 TinyGPS.h 라이브러리를 추가하고 시리얼 통신선 설정을 위한 SoftwareSerial.h를 불러들이는 구조이다.
#include <ESP8266WiFi.h>
#include <TinyGPS.h>
#include <SoftwareSerial.h>
⚫⚫⚫
setup() 으로 넘어 가기전에 일종의 전역 변수 내지는 상수들을 설정한다. GPS 네비게이션의 중요한 변수로는 위도(lat:latitude)와 경도(lon:longitude)를 들 수 있다. 이 두가지는 GPS 로부터 정수형 데이터로 센싱되며 TinyGPS로부터의 처리 결과도 역시 정수형이다. 따라서 지도를 불러 오기위해서 이들 데이터는 반드시 실수 형 즉 flon과 flat으로 변환되어야 한다. 이 프로그램에서는 소숫점 이하 6자리까지 정밀도를 유지하기 위하여 double precision 으로 정의하였다. ledPin은 NodeMCU 빌트인 LED on OFF를 위해 그대로 두었으며 cnt는 loop() 문에서 루프 도는 횟수를 셈하기 위해서 편의상 도입하였다.
//Latitude, Longgitude coordinate data
long lat,lon;
double flat;
double flon;
int ledPin = 2; //
int cnt = 0;
3종류의 라이브러리가 도입되었고 이들을 사용하기 위한 선언이 필요하다.
SoftwareSerial gpsSerial(5,3);
WiFiServer server(80);
TinyGPS gps;
⚫⚫⚫
setup() 에서는 아두이노 NodeMCU가 USB-2-Serial 통신을 위한 baud 속도 115200 설정이 필요하다. NodeMCU에서 이 숫자는 절대적이며 9600으로 바꾸어서는 안된다. 아울러 VK16E GPS 센서와의 통신은 baud 속도 9600으로 설정한다.
void setup() {
Serial.begin(115200);
⚫⚫⚫
Serial.println("Start GPS... ");
gpsSerial.begin(9600);//Good
}// end of setup
loop() 문의 초반에 GPS 데이터를 센싱하는 루틴을 두도록 한다. gpsSerial.read() 명령이 GPS 센서로부터 데이터를 읽어 들인다. TinyGPS.h 라이브러리의 지원에 따라 gps.encode() 명령을 통해 필요한 정보들을 받아 볼 수 있게 된다.
void loop() {
while(gpsSerial.available()){
delay(1);
if(gps.encode(gpsSerial.read())){
gps.get_position(&lat, &lon);
Serial.println("Position: ");
Serial.print("lat: ");
flat = lat;
flat = flat/1000000.0;
Serial.print(flat,DEC);
Serial.print(" ");// print latitude
⚫⚫⚫
위도와 경도 데이터 (lat,lon)은 8자리 정수 형태로 출력되므로 일차적으로 실수화 하고 그 다음 백만으로 나누어 준다. 이 데이터를 시리얼 모니터에 출력하거나 또는 스마트폰 HTML 프로그램에 사용해야 한다.
소숫점 이하 6자리를 유지하려면 반드시 자라 수를 명시 하든지 아니면 DEC 표현을 사용하도록 한다.
한편 지난번 블로그에서 아두이노 GPS로부터 받은 데이터를 검증하기 위해 www.w3schools.com 사이트의 편집기에서 HTML프로그램을 체크하였다. 구글 맵을 사용하는 경우와 다른 하나는 다음 맵을 사용하는 경우였다. 하지만 여기서 다루었던 다음 맵은 www.w3schools.com 사이트의 편집기에서는 실행이 되었으나 아두이노 NodeMCU 프로그램에서는 실행이 여의치 않아 많은 손질이 가해졌으며 이번 블로그에 첨부된 프로그램을 최종 본으로 참조하기 바란다.
HTML프로그램 도입 부에 배경 색과 버튼 색 설정을 위한 CSS 부분을 두었다. 그간에 여러 종류의 프로그램에서 이미 사용했었기에 부연 설명은 생략한다.
실질적으로 다음지도를 호출하기 위한 부분을 살펴보자.
스마트폰 웹화면에서 볼 지도 크기를 500X400으로 설정함과 동시에 텍스트 및 자바스크립트 도입 과 함께 다음지도 API 키 값을 설정한다.
client.println("<body>");
client.println("<div id='map' style='width:500px;height:400px;'></div>");
client.println("<script type='text/javascript' src=");
client.println("'//apis.daum.net/maps/maps3.js?apikey=다음지도 API 키 값 입력'></script>");
이 다음 부분부터가 자바스크립트 영역 프로그램에 해당한다. 프로그램에서 보듯이 위도 및 경도 데이터를 client.print() 명령을 사용하여 보내 줄 때에 반드시 double 선언된 변수 flat와 flon에 DEC를 명시해야 한다. 또는 DEC 대신 숫자 6으로 대체해도 무방하다.
아울러 불러온 지도에 마커를 표시하기 위해서도 한 번 더 위도 및 경도 데이터가 필요하다.
client.println("<script>");
client.println("var container = document.getElementById('map');");
client.println("var options = {");
client.println("center: new daum.maps.LatLng(");
client.print(flat,DEC);
client.print(", ");
client.print(flon,DEC);
client.println("),");
client.println("level: 4");
client.println("};");
client.println("var map = new daum.maps.Map(container, options);");
client.println("var markerPosition = new daum.maps.LatLng(");
client.print(flat,DEC);
client.print(", ");
client.print(flon,DEC);
client.println(")");
client.println("var marker = new daum.maps.Marker({position: markerPosition});");
client.println("marker.setMap(map);");
client.println("</script>");
client.println("<a href=\"/on\"\" class='button'>Refresh </button></a>");
client.println("</body>");
client.println("</html>");
스마트폰에서 지도 및 마커를 보기 위해서는 지도 배율을 조절할 필요가 있다. 다음 지도 프로그램 상에서는 level 이라는 변수인데 4가 적당하다.
VK 16E GPS에서 얻어진 위도 경도 데이터를 사용하여 상당히 신뢰할만한 불과 수 메터의 오차 결과를 볼 수 있었다. 그림에 표시된 마카 위치는 학술정보관 건물의 영향이 거의 없는 넓은 주차장이다.
일정 거리 이동 후 Refresh 버튼을 누르면 화면이 갱신 된다. 지도 확대 시 지도 자체를 확대하지 말고 백그라운드 컬러 부분을 확대하는 것이 좋다.
아래의 네비게이션 프로그램을 참조하기 바란다.
Webserver_nodemcu_daum_api_html_01
#include <ESP8266WiFi.h>
#include <TinyGPS.h>
#include <SoftwareSerial.h>
const char* ssid = "AndroidHotspot1234";//자신의 스마트폰 핫스팟 ID 입력
const char* password = "00000000";//자신의 스마트폰 핫스팟 비밀번호 입력
//Latitude, Longgitude coordinate data
long lat,lon;
double flat;
double flon;
int ledPin = 2; //
int cnt = 0;
SoftwareSerial gpsSerial(5,3);
WiFiServer server(80);
TinyGPS gps;
void setup() {
Serial.begin(115200);
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, HIGH);
// Set WiFi to station mode and disconnect from an AP if it was previously connected
WiFi.mode(WIFI_STA);
WiFi.disconnect();
delay(100);
Serial.println("Setup done");
// Connect to WiFi network
Serial.println();
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
// Start the server
server.begin();
Serial.println("Server started");
// Print the IP address
Serial.print("Use this URL to connect: ");
Serial.print("http://");
Serial.print(WiFi.localIP());
Serial.println("/");
Serial.println("Start GPS... ");
gpsSerial.begin(9600);//Good
}
void loop() {
while(gpsSerial.available()){
delay(1);
if(gps.encode(gpsSerial.read())){
gps.get_position(&lat, &lon);
Serial.println("Position: ");
Serial.print("lat: ");
flat = lat;
flat = flat/1000000.0;
Serial.print(flat,DEC);
Serial.print(" ");// print latitude
Serial.print("lon: ");
flon = lon;
flon = flon/1000000.0;
Serial.println(flon,DEC); // print longitude
delay(1000);
}
}
// Check if a client has connected
WiFiClient client = server.available();
if (!client) {
return;
}
// Wait until the client sends some data
Serial.println("new client");
while(!client.available()){
delay(1);
}
// Read the first line of the request
String request = client.readStringUntil('\r');
Serial.println(request);
client.flush();
// Match the request
int value = LOW;
if (request.indexOf("/on") != -1) {
Serial.print(request.indexOf("/on"));
digitalWrite(ledPin, LOW);
value = HIGH;
}
// Return the response
cnt = cnt + 1;
Serial.println(cnt);
client.println("<!DOCTYPE HTML>");
client.println("<html>");
//배경 색 문자 색 사이즈 HTML CSS 설정
client.println("<head>");
client.println("<style>");
client.println("body {");
client.println("background-color: lightblue;");
client.println("}");
client.println("p {");
client.println("color:red;");
client.println("font-size:250%;");
client.println("}");
//버튼 HTML CSS 설정
client.println(".button {");
client.println("background-color: #4CAF50;");
client.println("border: none;");
client.println("color: white;");
client.println("padding: 15px 32px;");
client.println("text-align: center;");
client.println("text-decoration: none;");
client.println("display: inline-block;");
client.println("font-size: 20px;");
client.println("margin: 14px 20px;");
client.println("cursor: pointer;");
client.println("}");
client.println("</style>");
client.println("</head>");
client.println("<body>");
client.println("<div id='map' style='width:500px;height:400px;'></div>");
client.println("<script type='text/javascript' src=");
client.println("'//apis.daum.net/maps/maps3.js?apikey=자신의 다음지도 API 키 값 입력 '></script>");
client.println("<script>");
client.println("var container = document.getElementById('map');");
client.println("var options = {");
client.println("center: new daum.maps.LatLng(");
client.print(flat,DEC);
client.print(", ");
client.print(flon,DEC);
client.println("),");
client.println("level: 4");
client.println("};");
client.println("var map = new daum.maps.Map(container, options);");
client.println("var markerPosition = new daum.maps.LatLng(");
client.print(flat,DEC);
client.print(", ");
client.print(flon,DEC);
client.println(")");
client.println("var marker = new daum.maps.Marker({position: markerPosition});");
client.println("marker.setMap(map);");
client.println("</script>");
client.println("<a href=\"/on\"\" class='button'>Refresh </button></a>");
client.println("</body>");
client.println("</html>");
delay(1000);
Serial.println("Client disonnected");
Serial.println("");
}//프로그램 끝