В данной статье представлена простая демонстрация проекта геозонирования (Geo-fencing) с использованием GPS-модуля и NodeMCU ESP8266. Geo-fencing (геозонирование, гео—ограждение) — это виртуальная граница или забор, который окружает физический участок. Он образует барьер между этим участком и окружающей средой, как реальный забор. В отличие от физического забора, Geo-Fencing с NodeMCU ESP8266 может обнаруживать движение внутри виртуального забора. Он может быть любого размера или формы.
Геозоны строятся с помощью картографического программного обеспечения, которое позволяет пользователю строить геозону в выбранном географическом регионе. Она состоит из набора координат, таких как широта и долгота, или, в случае круговой геозоны, одной точки, которая служит центром и радиусом.
В этой статье мы создадим динамический веб-сервер на основе AJAX (асинхронный JavaScript и XML) для отслеживания цели. Виртуальный забор будет создан путем маркировки вершин многоугольника на картах Google. Алгоритм «точка в многоугольнике» будет использоваться для определения того, находится ли цель внутри забора или за его пределами.
Возможно, вам будут интересны следующие статьи на нашем сайте, относящиеся к тематике данного проекта:
- как выбрать GPS модуль для своего проекта;
- подключение GPS-модуля Quectel L80 к Arduino;
- GPS-трекер на ESP32, модуле Quectel L86 GPS и OLED-дисплее.
Необходимые компоненты
В этом проекте геозонирования с помощью NodeMCU мы должны отправиться на поле для тестирования. Сделать это на уровне макетной платы — невыполнимая идея. Поэтому NodeMCU должен питаться от батареи. Я использовал одноэлементную батарею LiPo для питания оборудования. Я использовал TP4056 для безопасной зарядки и разрядки батареи.
- NodeMCU ESP8266 (купить на AliExpress).
- GPS-модуль Quectel L80 (купить на AliExpress).
- Модуль зарядного устройства аккумулятора TP4056 (купить на AliExpress).
- Аккумулятор емкостью 1000 мАч.
- Нажимной переключатель.
- Макетная плата.
- Соединительные провода/перемычки.
Реклама: ООО «АЛИБАБА.КОМ (РУ)» ИНН: 7703380158
Алгоритм «точка в многоугольнике»
Ключевой задачей в геозонировании с использованием ESP8266 является определение того, находится ли цель внутри забора или за его пределами. Забор может иметь любую форму. Поэтому нам нужно решить эту проблему, предположив, что забор представляет собой n-мерный многоугольник. Сложность алгоритма также играет решающую роль в эффективности отслеживания. Принимая во внимание эти опасения, я выбрал следующий алгоритм для этого проекта.
Предположим, что созданный пользователем забор представляет собой n-мерный многоугольник, и у нас есть координаты всех вершин многоугольника в формате Vn (xn , yn ). Пусть текущее местоположение точки отслеживания будет T (xt , yt ). Анимация ниже дает вам наглядное представление о том, как работает этот алгоритм.
Сумма углов между последовательными линиями, проведенными от точки отслеживания к вершинам, является решающим фактором, который определяет, находится ли цель внутри или вне ограждения. Суммирование может производиться как по часовой стрелке, так и против часовой стрелки.
Теперь давайте рассмотрим шаги для вычисления угла с помощью имеющихся у нас координат. Мы все знаем скалярное произведение двух векторов ( |ab| = |a|.|b|.cos(θ) ). Здесь пусть ‘ a ‘ будет вектором из точки отслеживания в вершину 1, а ‘ b ‘ будет вектором из точки отслеживания в вершину 2. Значение ‘ θ ‘ можно найти, оставив ‘ θ ‘ в одной части уравнения и перенеся оставшееся в другую.
Таким же образом можно вычислить и остальные углы.
Веб-сервер на базе AJAX
Мы создадим веб-сервер на основе AJAX для динамического мониторинга цели в реальном времени. Так чем же этот AJAX-сервер отличается от обычных веб-серверов? Давайте посмотрим на ответ на этот вопрос.
В обычном веб-сервере клиент отправляет запрос на сервер, а затем сервер отправляет полную страницу в качестве ответа клиенту. Всякий раз, когда вы выполняете какие-либо действия на текущей веб-странице, например, отправляете текст или перезагружаете ее для просмотра обновленных данных, клиент снова отправляет запрос на сервер, а сервер снова отправляет полную обновленную страницу клиенту в качестве ответа.
Однако с веб-серверами на основе AJAX после первой загрузки страницы на странице будет отображаться только обновленная часть данных без ручной перезагрузки. В результате веб-серверы на основе AJAX лучше всего подходят для динамического мониторинга данных в реальном времени.
Взаимодействие Quectel L80 с NodeMCU
В одной из предыдущих статей мы обсуждали, как соединить датчик Quectel L80 GPS с Arduino Nano. Взаимодействие модуля L80 GPS с NodeMCU ESP8266 примерно такое же.
Перейдите по этой ссылке, чтобы прочитать предыдущую статью о взаимодействии Quectel L80 с Arduino.
Мы будем использовать аналогичную схему для разработки нашего проекта геозоны с помощью NodeMCU ESP8266.
Схема проекта
Схема проекта геозонирования на модуле GPS и NodeMCU ESP8266 представлена на следующем рисунке.
Подключения схемы просты. Поскольку Quectel L80 основан на UART, мы можем использовать программный последовательный порт, чтобы подключить контакты RX и TX Quectel L80 к любым цифровым контактам NodeMCU ESP8266.
Аккумулятор подключен к TP4056. Узел MCU получает питание от выхода TP4056 через коммутатор.
Вот как выглядит оборудование после подключения. Я также заклеил оборудование, чтобы оно было под рукой.
Я также сделал корпус, напечатанный на 3D-принтере. Вы можете скачать файл STL и файл GCode для 3D-печати.
Теперь с аппаратной частью все хорошо. Можно приступать к программной части.
Печатная плата для проекта
Если вы не хотите собирать схему на макетной плате, а хотите печатную плату для проекта, то вот печатная плата для вас. Печатная плата для нашего проекта геозонирования разработана с использованием онлайн-инструмента EasyEDA. Печатная плата выглядит примерно так, как показано ниже.
Файл Gerber для изготовления данной печатной платы вы можете скачать по следующей ссылке.
Исходный код программы
Вот программа для нашего проекта, которую следует загрузить на плату NodeMCU ESP8266.
Если вы работаете не с Arduino IDE, а с PlatformIO, вы можете клонировать этот репозиторий и начать загрузку кода в NodeMCU, введя «pio run –target upload» в PlatformIO CLI или выбрав опцию загрузки. Не забудьте поставить звездочку репозитория после клонирования :).
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
#include <TinyGPS++.h> #include <SoftwareSerial.h> #include <ESP8266WiFi.h> #include <WiFiClient.h> #include <ESP8266WebServer.h> #include "index.h" #define M_PI 3.14159265358979323846264338327950288 #define rxGPS 5 #define txGPS 16 const double fences[1][10][2] = {{{17.529188, 78.361845}, {17.529840, 78.361919}, {17.529934, 78.362197}, {17.530624, 78.362507}, {17.530832, 78.363043}, {17.530999, 78.363451}, {17.530924, 78.363976}, {17.529248, 78.363288}, {17.529101, 78.362858}, {17.529040, 78.362489},} }; /*Variables to store AP credentials*/ String ssid = ""; String password = ""; int WiFiConnectMode = 1; /* 0: smart config; 1: hard code*/ double latitude, longitude; int sat; String date; char lati[12]; char longi[12]; int targetStatus; int fence; char cumulativeAngle[12]; int deviceStatus = 0; SoftwareSerial gpsSerial(rxGPS, txGPS); TinyGPSPlus gps; ESP8266WebServer gpsServer(80); void connectWifi(); void updateLatLon(); void pip(); void handleRoot(); void fenceSelect(); void gps_data(); void setup(){ Serial.begin(9600); gpsSerial.begin(9600); connectWifi(); gpsServer.on("/", handleRoot); gpsServer.on("/status", fenceSelect); gpsServer.on("/values", gps_data); gpsServer.begin(); } void loop(){ while (gpsSerial.available()){ deviceStatus = 1; updateLatLon(); pip(); gpsServer.handleClient(); } gpsServer.handleClient(); } void connectWifi(){ if(WiFiConnectMode == 0){ // Operate the ESP12E in wifi station mode for smart config WiFi.mode(WIFI_STA); // Begin the smart configuration to get the Access Point credentials WiFi.beginSmartConfig(); Serial.println("------------------------------------------------"); Serial.print("Waiting for SmartConfig "); while (!WiFi.smartConfigDone()) { delay(250); Serial.print("."); } Serial.println(); Serial.println("SmartConfig done."); // Print the AP credentials to the serial monitor ssid = WiFi.SSID(); password = WiFi.psk(); Serial.println("------------------------------------------------"); Serial.print("Acesspoint SSID : "); Serial.println(ssid); Serial.print("Acesspoint password : "); Serial.println(password); Serial.println("------------------------------------------------"); // Connect the ESP12E to the AP Serial.print("Connecting to Access Point "); while (WiFi.status() != WL_CONNECTED) { delay(100); Serial.print("."); } Serial.println(); Serial.println("Connected."); Serial.println("------------------------------------------------"); Serial.print("IP Address: "); Serial.println(WiFi.localIP()); Serial.println("------------------------------------------------"); } else{ String ssid = "vikas_phone"; String password = "addepalliVikas"; WiFi.begin(ssid,password); Serial.println("------------------------------------------------"); Serial.print("Connecting to Access Point "); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(""); Serial.print("Connected to "); Serial.println(ssid); Serial.print("IP address: "); Serial.println(WiFi.localIP()); Serial.println("------------------------------------------------"); } } void updateLatLon(){ Serial.print(gpsSerial.read()); Serial.print(" "); Serial.println(gps.encode(gpsSerial.read())); if (gps.encode(gpsSerial.read())) { //Serial.println("FunCheck2"); sat = gps.satellites.value(); latitude = gps.location.lat(); longitude = gps.location.lng(); dtostrf(latitude,9,7,lati); dtostrf(longitude,9,7,longi); Serial.print("SATS: "); Serial.println(sat); Serial.print("LAT: "); Serial.println(latitude,6); Serial.print("LONG: "); Serial.println(longitude,6); Serial.print("ALT: "); Serial.println(gps.altitude.meters()); Serial.print("SPEED: "); Serial.println(gps.speed.mps()); Serial.print("Date: "); date = String(gps.date.day())+"/"+gps.date.month()+"/"+gps.date.year(); Serial.println(date); Serial.print("Hour: "); Serial.print(gps.time.hour()); Serial.print(":"); Serial.print(gps.time.minute()); Serial.print(":"); Serial.println(gps.time.second()); Serial.println("---------------------------"); Serial.println("FunCheck3"); } } void pip(){ int fenceSize = sizeof(fences[fence - 1])/sizeof(fences[fence - 1][0]); double vectors[fenceSize][2]; for(int i = 0; i < fenceSize; i++){ vectors[i][0] = fences[fence - 1][i][0] - latitude; vectors[i][1] = fences[fence - 1][i][1] - longitude; } double angle = 0; double num, den; for(int i = 0; i < fenceSize; i++){ num = (vectors[i%fenceSize][0])*(vectors[(i+1)%fenceSize][0])+ (vectors[i%fenceSize][1])*(vectors[(i+1)%fenceSize][1]); den = (sqrt(pow(vectors[i%fenceSize][0],2) + pow(vectors[i%fenceSize][1],2)))*(sqrt(pow(vectors[(i+1)%fenceSize][0],2) + pow(vectors[(i+1)%fenceSize][1],2))); angle = angle + (180*acos(num/den)/M_PI); } dtostrf(angle,9,7,cumulativeAngle); if(angle > 355 && angle < 365) targetStatus = 1; else targetStatus = 0; } void handleRoot(){ String s = webpage; gpsServer.send(200, "text/html", s); } void fenceSelect(){ fence = gpsServer.arg("fenceValue").toInt(); gpsServer.send(200, "text/plane", String(fence)); } void gps_data(){ String payload = String(sat) + "#" + date + "#" + lati + "#" + longi; if(targetStatus == 0) payload = payload + "#outside"; else payload = payload + "#inside"; payload = payload + "#" + cumulativeAngle; if(deviceStatus == 0) payload = payload + "#offline"; else payload = payload + "#online"; gpsServer.send(200, "text/plane", payload); } |
Веб-сервер на основе AJAX содержится в этом заголовочном файле. Если вы используете Arduino IDE, убедитесь, что этот заголовочный файл и файлы «.ino» находятся в одном каталоге. Если вы используете PlatformIO, просто клонируйте вышеупомянутый репозиторий.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
const char webpage[] PROGMEM = R"=====( <!DOCTYPE html> <html> <style type="text/css"> body { background-image: url('https://images.unsplash.com/photo-1488866022504-f2584929ca5f?auto=format&fit=crop&w=1486&q=80&ixid=dW5zcGxhc2guY29tOzs7Ozs%3D'); background-size: cover; margin: 0; background-repeat: no-repeat; background-position: 0 0; transition: 2s cubic-bezier(0.645, 0.045, 0.355, 1); color :white; } hr { visibility: visible; border: 0; height: 1px; background-image: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0)); } .button { background-color: rgba(55, 72, 87, 0.8); border: none; color: white; padding: 12px 20px; text-align: center; text-decoration: none; display: inline-block; border-radius: 12px; font-size: 16px; } </style> <body style="background-color: #ffffff ; font-family:verdana"> <center> <p>Device Status <span id = "Device_status" style="color : rgba(0, 255, 106, 0.857);">offline</span></p> <hr style = "width: 70%;"> <h1>Dashboard</h1> <hr style = "width: 70%;"> <div> <button class="button" onclick="send(1)">Fence 1</button> <button class="button" onclick="send(2)">Fence 2</button> <p>Selected Fence: <span id="fence_id">0</span><br></p> </div> <hr style = "width: 40%;"> <div style="background-color: rgba(89, 154, 211, 0.3);; width: 30%; border-radius: 12px;"> <table style="text-align: center; " > <tr> <th style="padding: 0 20px 0 20px;">Date</th> <th style="padding: 0 20px 0 20px;">No.of Sats</th> </tr> <tr> <td><span id="date">0</span><br></td> <td><span id="sats">0</span><br></td> </tr> </table> </div> <hr style = "width: 40%;"> <div> <div style="background-color: rgba(89, 154, 211, 0.3); display: inline-block; width: 10%; border-radius: 12px;"> <table style="text-align: center; " > <tr> <th>Latitude</th> </tr> <tr> <td><span id="lat_val">0</span><br></td> </tr> <tr> <th>Longitude</th> </tr> <tr> <td><span id="lon_val">0</span><br></td> </tr> </table> </div> <div style="background-color: rgba(89, 154, 211, 0.3); display: inline-block; width: 15%; border-radius: 12px;"> <table style="text-align: center; " > <tr> <th>Target Status</th> </tr> <tr> <td><span id="target_status">0</span><br></td> </tr> <tr> <th>Cumulative Angle</th> </tr> <tr> <td><span id="angle">0</span><br></td> </tr> </table> </div> </div> <hr style = "width: 70%;"> <script> function send(fence_val) { var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { document.getElementById("fence_id").innerHTML = this.responseText; } }; xhttp.open("GET", "status?fenceValue="+fence_val, true); xhttp.send(); } setInterval(function() {getData();}, 2000); function getData() { var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { const data = this.responseText.split("#"); document.getElementById("date").innerHTML = data[1]; document.getElementById("sats").innerHTML = data[0]; document.getElementById("lat_val").innerHTML = data[2]; document.getElementById("lon_val").innerHTML = data[3]; document.getElementById("target_status").innerHTML = data[4]; document.getElementById("angle").innerHTML = data[5]; document.getElementById("Device_status").innerHTML = data[6]; } }; xhttp.open("GET", "values", true); xhttp.send(); } </script> </center> </body> </html> )====="; |
Объяснение кода
Я использовал библиотеку TinyGPS++ для декодирования операторов NMEA, считываемых с Quectel L80. Библиотеку SoftwareSerial для создания виртуального последовательного порта. Библиотека ESP8266WebServer используется для создания веб-сервера на базе AJAX.
Функция «connectWifi()» позволяет NodeMCU подключиться к домашней точке доступа (AP). Переменная «WiFiConnectionMode» определяет режим подключения к AP. Если переменная установлена на «0», устройство подключается к AP через интеллектуальную конфигурацию, а если переменная установлена на «1», устройство подключается к AP, используя учетные данные AP, которые жестко закодированы в переменных «ssid», «password» внутри функции. Вы можете использовать приложение «EspTouch», которое доступно в магазине Google Play, если вы решите использовать NodeMCU в режиме мягкой конфигурации.
Функция «updateLatLon()» обновляет переменные широты, долготы, высоты, скорости и даты при каждом вызове функции.
Функция «pip()» обновляет переменную «targetStatus» всякий раз, когда она вызывается. Она реализует алгоритм «точка в многоугольнике» с использованием обновленных координат. Если цель находится внутри ограждения, переменная «targetStatus» устанавливается в «1», в противном случае — в «0».
«handleRoot()», «fenceSelect()» и «gps_data()» являются обработчиками. Эти обработчики предназначены для отправки соответствующих данных на клиентскую сторону при запросе. «handlerRoot()» предназначен для обработки корневой веб-страницы и отправки всей веб-страницы. «gps_data()» предназначен для обработки данных GPS, этот обработчик преобразует широту, долготу, количество спутников, дату, статус цели и кумулятивный/суммарный угол (θsum) в строку, разделенную «#». Обработчик «fenceSelect()» получает выбранную информацию о геозоне от веб-сервера.
Создание забора на картах Google и жесткое кодирование координат
Откройте карты Google, отметьте место, которое вы планируете сделать вершиной забора, и подпишите вершину, а также запишите координаты. Повторите процедуру для оставшихся точек. Ниже представлен забор, который я создал и использовал в этом проекте.
Теперь жестко закодируйте широту и долготу забора в переменную fence, которая является трехмерным массивом. Третье измерение — для хранения координат различных гео-зон.
Тестирование работы проекта
Загрузите код, выбрав соответствующие настройки в IDE, которую вы используете (Arduino или PlatformIO). Если вы используете PlatformIO, после загрузки кода вы увидите что-то вроде нижеприведенного в PlatformIO CLI.
После этого откройте последовательный монитор. В нем вы должны увидеть примерно следующую картину.
Откройте этот IP-адрес в браузере. Теперь вы можете найти веб-сервер с пустыми значениями, как показано ниже.
Откройте карты Google и веб-сервер в режиме разделенного экрана, чтобы увидеть перемещение цели вместе с гео-зоной, а также ее статус на панели управления. Нижеприведенный gif показывает конечный результат.
На приведенном выше gif-изображении четко видна геозона. Когда точка привязки пересекает геозону, переменная статуса цели на панели инструментов обновляется до значения «вне» и наоборот. А также другие данные, такие как широта, долгота, количество подключенных спутников и дата, также можно увидеть на панели инструментов.
Вот как можно создать проект геозонирования с помощью платы NodeMCU ESP8266. Беспроводная версия проекта Geo Fecing может быть создана с использованием модуля LoRa.
