Метеостанция на Arduino от А до Я. Часть 5

Окончание. Предыдущая часть.

Оглавление:

Поговорим о программном обеспечении заоконного датчика. После этого у вас получится законченная система с которой уже можно экспериментировать.

Напомню, что сервер — это центральный, домашний блок, который может общаться с интернет через WiFi, а клиент — это удалённый, заоконный датчик, который передаёт данные на сервер по радиоканалу.

Исходный код как для сервера, так и для клиента находится здесь.
Исходные тексты снабжены подробными комментариями.

На клиенте почти ничего настраивать не надо.

Радиопередатчик nRF24L01+, точнее библиотека RadioHead, требует указания адресов сервера и клиента. Адреса предусмотрены на тот случай, если у вас будет серверов и клиентов больше чем один. Адрес — это просто любое целое число. Когда клиент посылает пакет с данными на сервер, то он указывает для какого сервера предназначен этот пакет. Сервер зная свой собственный адрес, в свою очередь, определяет для него ли предназначен этот пакет.

Поэтому SERVER_ADDRESS на сервере и на клиенте должен совпадать, а вот CLIENT_ADDRESS для разных клиентов должен отличаться. Другими словами, если к нашей системе вы в будущем подключите еще один новый датчик, то CLIENT_ADDRESS для него нужно будет изменить.

// Адрес сервера и клиента
#define SERVER_ADDRESS 10
#define CLIENT_ADDRESS 20 // ИЗМЕНИТЬ для другого экземпляра !!!

Номер радиоканала RF_CHANNEL должен быть одинаковым у всех. По умолчанию он равен 2. Я изменил номер по умолчанию, вы можете выбрать любой другой.

// Номер радиоканала. Должен быть КАК И У СЕРВЕРА
#define RF_CHANNEL 73

Настройки вольтметра для измерения питающего напряжения батареи необходимо изменить:

// Резисторы делителя напряжения, фактические значения
const float r1 = 100400; // 100K
const float r2 = 9960; // 10K
// Эту константу необходимо откалибровать индивидуально
// как описано здесь http://localhost/arduino-secret-true-voltmeter/
const float typVbg = 1.082; // обычно в пределах 1.0 -- 1.2 В

Для экономии энергии используется библиотека Lightweight low power library for Arduino.

Вот мои замеры фактического потребления для Arduino Pro Mini с этой либой :


  • обычно 25mA
  • при работе с DHT то же самое
  • при радио передаче 38 mA
  • при LowPower.idle 15 mA
  • при LowPower.powerDown 7.5 mA

Клиент делает замеры температуры, влажности и напряжения питания, упаковывает всё это в структуру данных, отсылает данные на сервер и «засыпает». Если при передаче произошли ошибки, то передача тут же повторяется.

Сервер (центральный, домашний блок) в свою очередь данные принимает, подтверждает прием и обрабатывает их.

После проделанной работы у нас есть вполне работоспособная конструкция метеостанции. Но сейчас таких метеостанций пруд пруди, локальные поделки это уже не модно. У нас ведь интернет вещей.

Поэтому поговорим о том, как осуществляется выход в эти ваши интернеты, приделаем к нашей метеостанции базу данных и веб-морду к ней.

Постановка задачи для «вебки» :


  • принимать и хранить данные метеостанции: температура, влажность, атмосферное давление, напряжение питания
  • отображать эти данные
  • строить графики.

При этом нам понадобится хостинг с поддержкой Apache, PHP и MySQL с модулем mysqli. И этим условиям удовлетворяет практически любой хостинг на планете Земля. Либо вместо хостинга будет ваш компьютер играющий роль сервера, подключённый к домашнему сетевому маршрутизатору и имеющий выход в Интернет.


Создание базы данных

Начнем с самого начала, а именно с проектирования и создания базы данных.

Базы данных это свой мир и изучать его можно долго, поэтому бегло коснёмся только тех вещей, которые нам непосредственно необходимы.

Все SQL скрипты находятся в каталоге weather-station/server/php-sql/

С чего начинается проектирование БД? С логического и физического представления.

Логическое представление или схема базы данных:


  • таблица с данными DHT датчика температуры и влажности
  • таблица с данными BMP датчика давления и температуры
  • указанные таблицы не имеют связей между собой, точнее связи не нужны.

Физическая схема опирается на конкретную СУБД и типы данных. Проще разбирать на конкретном примере. SQL скрипт make_tables.sql раскрывает логическую и физическую схемы.

В каждой таблице должно быть поле типа

id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT

Имя поля может отличаться в разных БД, но смысл один — это уникальный идентификатор, ключ записи. На будущее, если вы видите базу данных в таблицах которой нет подобного счётчика, знайте, эту БД проектировал человек весьма далёкий от программирования, скорее всего гуманитарий.

Данные от однотипных датчиков храним в одной таблице, для датчиков другого типа создаём еще таблицу. Это чуть усложняет базу данных и PHP обвязку к ней, но это упрощает расширение или модификацию всей системы в дальнейшем.

В нашем проекте две таблицы. В таблице arduino_dht хранятся данные от датчика (ов) типа DHT (температура, влажность), в таблице arduino_bmp хранятся данные от датчика (ов) типа BMP (температура, давление). Если вы в будущем захотите иметь, например, датчик газов или детектор движения, то создаёте дополнительные таблицы, не поленитесь. Если подключаете ещё один датчик типа DHT11 или DHT22, то создавать дополнительную таблицу не нужно, используете таблицу arduino_dht. Надеюсь принцип ясен: отдельная физическая сущность — отдельная таблица.

Если в таблице будут храниться данные от несколько однотипных датчиков, то как же их различить? Для этого в каждой таблице вводится поле

idSensor INTEGER

Фактически это CLIENT_ADDRESS который мы прописывали в файле client/client.ino для каждого экземпляра удаленного датчика-клиента и в server/server.ino для датчика, который подключен непосредственно к серверу — центральному блоку.

В промышленных системах должна быть ещё одна таблица — соответствия idSensor и его словесного, человекочитаемого описания. Например, датчик с idSensor = 2 это «Температура, влажность в квартире» и т.д. Но в нашем проекте не будем усложнять, просто помните, что :


  • датчик с idSensor, он же CLIENT_ADDRESS, равным 11 — это есть домашний датчик на сервере — центральном блоке,
  • датчик с idSensor, он же CLIENT_ADDRESS, равным 20 — это первый (в нашем проекте и единственный) заоконный датчик-клиент.

Далее. В таблицах хранятся следующие данные:


  • ipRemote — IP адрес метеостанции (сервера) с которого пришли данные, полезно для отладки и мониторинга,
  • dateCreate — дата время создания записи,
  • millis — полезно для отладки, это время в миллисекундах с момента начала выполнения скетча на Arduino,
  • temperature — температура,
  • humidity — влажность,
  • voltage — напряжение питания,
  • pressure — давление,
  • errors — количество ошибок (не используется). Задумывалось для хранения количества ошибок при передаче и т.п., чтобы можно было удаленно оценить состояние всей системы.

Как видим таблицы arduino_dht и arduino_bmp очень похожи, отличие только в полях pressure и humidity, и возникает желание свалить всё в одну кучу (таблицу). Но делать так не велит первая нормальная форма, множество начинающих программистов пытались её обойти, но ни у одного не получилось, и мы не будем. Это как не замечать закон всемирного тяготения, до поры до времени вполне может получиться.

Таблица arduino_error_log полезна при отладке — это журнал ошибок и прочих системных сообщений.

Создание БД и её пользователя с правами описано в make_db.sql

-- см. также config.php
-- создать БД
CREATE DATABASE IF NOT EXISTS db_weather;

-- создать пользователя
CREATE USER 'u_weather'@'localhost' IDENTIFIED BY '***PASSWORD***';
GRANT ALL ON db_weather.* TO 'u_weather'@'localhost';

Это делается один раз, имя БД и имя пользователя можете придумать свои. И что точно необходимо сделать — это задать свой пароль.


PHP и веб-сервер

Все настройки веб-интерфейса хранятся в config.php. Измените его в соответствии с вашими настройками базы данных.

Задайте свой часовой пояс в формате PHP

date_default_timezone_set(‘Europe/Prague’);

Все доступные часовые пояса описаны здесь.

Задайте свой секретный ключ для доступа (в виде числа) который должен совпадать с константой SOURCE_KEY из скетча server.ino

$access_key = ‘***KEY***’;

В нашем веб-сервере нет авторизации, входа по паролю, это усложнило бы всю конструкцию. Для прототипа это не нужно. Поэтому вся защита построена на файле robots.txt, отсутствии index.php и на этом секретном ключе для доступа.

Основной PHP скрипт weather.php принимает простой HTTP GET запрос с данными и сохраняет их в соответствующие таблицы базы данных. Если ключ $access_key не совпадает, то запрос будет отвергнут.

Скрипт weather-view.php используется для просмотра таблиц данных и содержит гиперссылки на остальные скрипты веб-интерфейса. Вызывайте его так

http://ваш хост/ваш путь/weather-view.php?k=ваш access_key

Например

http://yourhost/iot/weather-view.php?k=12345

weather-view.php выводит простые таблички, где надо помнить, что :


  • датчик с id 11 это домашний датчик на сервере,
  • датчик с id 20 это заоконный датчик.

Скрипт function.php содержит функции, общие для всех PHP скриптов.

Скрипт chart-dht.php отвечает за рисование графиков при помощи Google Charts. Вот, например, график питающего напряжения заоконного датчика. Напряжение повышается в солнечный день за счёт солнечной же батареи и затем блок питания на аккумуляторах постепенно разряжается.

График

export-dht.php экспортирует данные из таблиц базы данных MySQL в файл формата CSV. Для дальнейшего импорта и анализа в электронных таблицах.

export-voltage.php экспортирует данные о напряжении питания заоконного датчика из базы данных MySQL в файл формата CSV. Полезно для отладки.

truncate.php очищает все таблицы, т.е. удаляет все наши данные. Полезно для отладки. На этот скрипт нет ссылок из weather-view.php, поэтому вызывать его надо по прямой ссылке в адресной строке браузера с указанием $access_key.

При приёме данных повсеместно используется функция mysqli_real_escape_string() для предотвращения попадания в БД некорректных значений.

В корень вашего сайта не забудьте положить robots.txt для предотвращения попадания в поисковые системы.

И вот теперь возвращаемся к скетчу server.ino, к той его части, которая соединяется с точкой доступа WiFi и отсылает данные на веб-сервер.

Как я уже писал, мне не удалось найти нормальную библиотеку для Arduino для управления модулем ESP8266 с помощью AT команд, пришлось «колхозить» самому. Напомню так же, что вам придется прошить в ESP8266–01 прошивку определённой версии. И теперь когда всё готово, разберём как это работает.

Для доступа к веб серверу в скетче server.ino необходимо изменить вот эти константы

const String DEST_HOST = "ваш хост"; // например habr.com
const String DEST_PORT = "ваш порт"; // обычно 80
const String DEST_URL = "/ваш путь/weather.php";
const String SOURCE_KEY= "ваш ключ доступа"; // должен совпадать с $access_key из config.php

В server.ino в функции void setup() сначала производится переключение ESP8266 в режим Station, т.е. он начинает работать как WiFi клиент

espSendCmd(«AT+CWMODE_CUR=1», «OK», 3000);

и далее следует подключение к точке доступа

espState = espConnectToWiFi();

Если подключения не происходит, то попытка повторяется (однократно)

if ( espState != ESP_SUCCESS ) {
  delay(5000);
  Serial.println("WiFi not connected! Try again ...");
  espConnectToWiFi();
}

Затем выбирается режим одиночного подключения TCP/IP

espSendCmd("AT+CIPMUX=0", "OK", 2000);

При отсылке данных от датчиков типа DHT на вебсервер используется функция с указанием типа данных как type=dht

espSendData( "type=dht&t=" + String(dhtData.temperature) + "&h=" + String(dhtData.humidity) + "&v=" + String(dhtData.voltage) + "&s=" + String(CLIENT_ADDRESS) );

При отсылке данных от датчиков типа BMP на вебсервер используется та же функция с указанием типа данных как type=bmp

espSendData( "type=bmp&t=" + String(temperature_bmp) + "&p=" + String(pressure_bmp) + "&s=" + String(CLIENT_ADDRESS) );

На вход функция espSendData() принимает строку HTTP GET запроса и отправляет её по назначению на веб-сервер.

Внутри себя espSendData() проверяет доступность ESP модуля, посылая ему команду «AT», далее проверяется подключение к WiFi и производится переподключение, если необходимо. Затем отправляются данные и соединение TCP закрывается.

В наше время, когда уже каждый может мигать светодиодом, никакой метеостанцией никого не удивишь. Но если поделка умеет связываться с сервером через WiFi, имеет веб-морду и мобильное приложение, то это уже кое-что! Под сервером здесь имеется в виду конечно же сервер приложений, т.е. в нашем случае это PHP-обвязка и СУБД MySQL. Не достаёт вишенки на торте, а именно приложения под Android написанием которого мы сейчас и займёмся.


Архитектура

Архитектура всей программной платформы метеостанции проста:


  • серверная часть (центральный блок) метеостанции собирает данные с удалённых датчиков-клиентов
  • далее она же передаёт данные на веб-сервер приложений, который сохраняет эти данные в базу данных
  • приложение на Андроид (или любое другое удалённое приложение: iOS, браузер) запрашивает данные у веб-сервера и отображает их на экране.

На экране Android устройства мы будем отображать текущие, самые последние показания датчиков.


HTTP GET и JSON

Вопрос, который нужно решить в первую очередь — это каким образом будет происходить передача данных от веб-сервера к Андроид-приложению.

Придумывать тут ничего не нужно, всё уже придумано за нас — это HTTP GET и JSON.

В нашем случае простой GET запрос к веб-серверу можно составить и отладить вручную, пока Андроид приложение ещё не готово.

В Java и в Android есть готовые библиотеки для обработки данных в формате JSON. JSON текстовый формат, читается человеком, что полезно для отладки.

Для того, чтобы сформировать текущие данные от датчиков метеостанции создадим на веб-сервере новый PHP скрипт last-data-to-json.php.

Вызов скрипта :

http://<хост>/last-data-to-json.php?k=

где , как мы помним, это секретный ключ доступа к БД.

Пример ответа в формате JSON:

{
    "DHT 11":{
        "idSensor":"11",
        "dateCreate":"2016-04-20 18:06:03",
        "temperature":"19",
        "humidity":"26",
        "voltage":"5.01"
    },
    "DHT 20":{
        "idSensor":"20",
        "dateCreate":"2016-04-18 07:36:26",
        "temperature":"10",
        "humidity":"26",
        "voltage":"3.7"
    },
    "BMP 11":{
        "idSensor":"11",
        "dateCreate":"2016-04-20 18:06:22",
        "temperature":"19",
        "pressure":"987.97"
    }
}

Необходимо напомнить, что у нас 3 датчика. Их ID и тип (DHT или BMP) жёстко закодированы по всему коду метеостанции. Такой способ хардкордного кодирования идеологически неверен, но для наколенного прототипа (где необходимо быстрое и простое решение) это разумный компромисс.

$idSensor = 11; // домашний DHT датчик
$idSensor = 11; // домашний BMP датчик
$idSensor = 20; // заоконный DHT датчик

Скрипт last-data-to-json.php берет из БД самые последние данные от этих разнотипных датчиков и упаковывает в формат JSON. Выборка данных из БД «с конца» просходит таким способом :

SELECT <поля> FROM <таблица> ORDER BY id DESC LIMIT 1;


Android

Теперь напишем простое приложение для Андроид, которое запрашивает, получает, декодирует JSON-данные и отображает информацию на экране.

Наше Android-приложение будет простым насколько это возможно, только сама суть технологии. Далее вокруг этого «скелета» уже можно будет наворачивать различные «красивости».

Вот скриншот того, что должно получиться в итоге

Android

Как видим UI просто спартанский, основан на LinearLayout, ничего лишнего.

В верхней части TextView показывает ID датчиков и их метео-данные. Кнопка «Обновить» инициирует повторный запрос к веб-серверу. Далее в EditText расположена единственная настройка программы — это URL запроса в виде

http://<ваш хост>/last-data-to-json.php?k=

Что необходимо отметить?

В манифест добавьте строки разрешающие интернет и проверку состояния сетевого соединения :



Работа с сетью и получение данных с веб-сайта происходит следующим образом.

Используем AsyncTask, чтобы создать фоновую задачу отдельно от главного потока пользовательского интерфейса. Эта фоновая задача берет URL запроса и использует его для создания HttpURLConnection.

После того, как соединение установлено, AsyncTask загружает содержимое веб-страницы (JSON) как InputStream. Далее InputStream преобразуется в строку, которая декодируется с помощью JSONObject и отображается в пользовательском интерфейсе методом onPostExecute().

В MainActivity.java измените URL на ваш :

private static final String 
  defUrl = "http://host/dir/last-data-to-json.php?k=<ваш ключ>";

он будет использоваться по умолчанию при первом запуске Android приложения.


Эпилог

Ну вот, что-то уже заработало. Далее можно что-то оптимизировать, что-то заменить, можно все выкинуть, но и что-то позаимствовать.

Отдельный большой больной вопрос — энергопотребление. Рекомендую почитать комментарии к постам, где много дельных советов.

To infinity… and beyond.

© Habrahabr.ru