В данной статье я расскажу о том, как сделать собственный погодный монитор, который будет самостоятельно запрашивать текущую погоду в выбранном городе по интернету и выводить соответствующую информацию на цветную светодиодную матрицу.
Собственно, первая проблема, которая встает перед нами — откуда брать погоду? С какого сайта? Я думаю, всем известны такие сайты, как Гисметео или Яндекс.Погода. Но в нашем проекте мы не будем брать у них метеосводку. Мы воспользуемся сайтом, про который, я думаю, вы и не слышали: http://openweathermap.org/.
Он бесплатный, и имеет возможность наложения метеоданных на карту. Но самым важным фактом для нас является то, что у него есть бесплатный, простой API, силу которого мы и используем в нашем устройстве. У Гисметео и Яндекс.Погоды API с тем же уровнем простоты нет.
Итак, допустим, нам необходимо узнать погоду в городе Москве. Мы можем найти соответствующую страницу через поиск. Открыв её, мы увидим примерно следующую картину.
Это удобно для человека, но как быть контроллеру? Ему же желательно скачивать из интернета поменьше данных, чтобы упростить их обработку. Для этого-то и воспользуемся API. Чтобы получить погоду в Москве, в метрической системе единиц и в более-менее читаемом формате XML, необходимо перейти по следующей ссылке:
http://api.openweathermap.org/data/2.5/weather?q=Moscow&mode=xml&units=metric
В этот раз не будет никаких картинок, только немного текста примерно такого содержания:
<current> <city id="524901" name="Moscow"> <coord lon="37.615555" lat="55.75222"/> <country>RU</country> <sun rise="2013-07-10T01:00:09" set="2013-07-10T18:09:50"/> </city> <temperature value="24.39" min="23" max="28.2" unit="celsius"/> <humidity value="36.8" unit="%"/> <pressure value="988.7" unit="hPa"/> <wind> <speed value="1.5" name=""/> <direction value="349.501" code="" name=""/> </wind> <clouds value="32" name="scattered clouds"/> <precipitation mode="no"/> <weather number="802" value="scattered clouds" icon="03d"/> <lastupdate value="2013-07-10T14:43:14"/> </current>
Нам интересна третья с конца строчка.
Всего API openweathermap в параметре icon
может содержать 18 значений формата «число-буква»:
Таблица соответсвия иконок в формате PNG (Portable Network Graphics)
День | Ночь | Состояние |
---|---|---|
01d | 01n | Чистое небо |
02d | 02n | Малооблачно |
03d | 03n | Рваная облачность |
04d | 04n | Облачно с прояснениями |
09d | 09n | Ливневый дождь |
10d | 10n | Дождь |
11d | 11n | Гроза |
13d | 13n | Снег |
50d | 50n | Туман |
Мы будем ориентироваться только на первые 2 цифры. Для простоты примера не будем различать день и ночь, а также поддержим только самые распространённые варианты текущей погоды.
Как вы помните, наша светодиодная матрица имеет разрешение 8x8 пикселей. И поэтому наши картинки должны быть такого же размера.
Вот четыре наших картинки: дождь, снег, солнце и гроза.
Такие картинки делаются очень просто. Достаточно поискать в интернете «Online icon editor». Там можно попиксельно нарисовать их и сохранить себе, к примеру, в формате PNG).
Проблема в том, что для нас такой формат не подходит, поскольку изображение в нем сжато и для работы с ним его необходимо распаковать. А это делается по достаточно сложному алгоритму. Нам бы хотелось иметь данные в каком-нибудь простом несжатом формате.
Есть специальный формат, называемый PBM (PortaBle anyMap). Изображение в нем представляется в обыкновенном текстовом формате. Вот пример нашей иконки со снегом в этом формате:
P1 # CREATOR: GIMP PNM Filter Version 1.1 8 8 0011110001000010100000011111111101010000001001010101001000000101
Файл содержит всего 4 строки:
P1
— идентификатор формата. В данном случае — PBM.8 8
— количество строк и столбцовМожно немного изменить последовательность, чтобы было чуть лучше видно, что это снег. Просто добавим в нужных местах переносы строк. Обратите внимание нарасположение единиц и нулей: они формируют картинку.
00111100 01000010 10000001 11111111 01010000 00100101 01010010 00000101
Как уже было видно чуть раньше, эти файлы были созданы в бесплатном графическом пакете GIMP, скачать который можно с официального сайта.
Необходимо открыть нужную нам иконку в этом графическом редакторе, затем выбрать Файл → Экспортировать
. А затем выбрать необходимый нам формат Изображение PPM (PBM)
.
Наши иконки мы назовём 01.pnm
, 10.pnm
, 11.pnm
, 13.pnm
, т.е. таким образом, чтобы имена файлов соответствовали числам, возвращаемым через API Open Weather Map.
Полученный PBM-файлы необходимо скопировать на карту памяти, а затем вставить ее в слот на Arduino Ethernet.
Поскольку мы используем светодиодный индикатор не напрямую, а как готовый модуль, то это в значительной степени облегчает подключение его к Arduino. Модуль индикатора работает через шину SPI, поэтому нам потребуется всего 3 провода для данных и 2 — для питания.
Нижняя часть модуля индикатора представлена на рисунке ниже.
Нам необходим разъем, который обведен на картинке прямоугольником. Распиновка у него следующая:
Номер | Назначение |
---|---|
1 | CLK — синхроимпульсы |
2 | LATCH — выбор устройства |
3 | DATA — данные |
В нижнем ряду этого разъёма находятся пины для подключения питания.
Все остальные разъемы нужны для того, чтобы иметь собирать из этих модулей большие матрицы.
Используемый нами Arduino Ethernet не имеет собственного USB-разъема и чипа, служащего посредником между USB и основным микроконтроллером. Поэтому для подключения его к компьютеру необходим преобразователь USB-Serial. С него же будет браться питание для Arduino.
На самом деле, мы использовали Arduino Ethernet с установленным PoE (Power Over Ethernet) модулем, поэтому при желании вы можете получать питание прямо из витой пары, и ко всему устройству в этом случае будет тянуться всего один провод. Однако для использования PoE, ваша сеть должна это поддерживать: существуют как роутеры, «заряжающие сеть», так и специальные, отдельные устройства, называющиеся PoE-инжекторами.
Необходимо учитывать тот факт, что Ethernet, SD-карта и индикатор работают по одному интерфейсу SPI. Его прелесть в том, что каждое новое устройство занимает всего один дополнительный пин (chip select), а 3 пина для обмена данными (11-й, 12-й и 13-й для Arduino Uno, Arduino Ethernet и подобных плат) остаются одними и теми же. Обратной стороной медали является тот факт, что одновременно общение может происходить лишь с одним устройством на шине.
Мы будем работать поочерёдно с Ethernet и SD, но нашу матрицу мы вообще полностью подключим к другим пинам, чтобы её работа никак не зависела и не пересекалась с работой Ethernet и SD. Это обусловлено тем, что динамическая индикация, которая реализована матрицей-модулем требует частого и постоянного обмена данными между собой и микроконтроллером.
Мы хотим следующего:
Для этого, скетч Arduino может выглядеть так:
#include <rgb_matrix.h> #include <SPI.h> #include <SD.h> #include <Ethernet.h> #define SD_CHIP_SELECT_PIN 4 #define ETHERNET_CHIP_SELECT_PIN 10 // Массив-изображение: 1 байт (8 бит) -- одна строка из 8 // светодиодов char iconBytes[8]; // IP-адрес (укажите тот, который свободен в вашей сети) IPAddress ip(192, 168, 10, 141); // MAC-адрес указан на наклейке, на обороте платы byte mac[] = { 0x5E, 0xBD, 0x3E, 0xEF, 0xFE, 0xBD }; // Домен сервера, на который мы будем обращаться const char server[] = "api.openweathermap.org"; // Маска, по которой мы ищем название иконки const char token[] = "icon=\"???"; // Сюда будет сохраняться номер иконки из ответа API char imageId[12] = "no"; // Объект для работы с enthernet-сетью EthernetClient client; // Номера пинов и размер рабочего поля в индикаторе #define N_X 1 #define N_Y 1 #define DATA_PIN 5 #define CLK_PIN 6 #define LATCH_PIN 2 // Объект для работы с матрицей rgb_matrix marix = rgb_matrix(N_X, N_Y, DATA_PIN, CLK_PIN, LATCH_PIN); // Время последнего обновления unsigned long startTime = 0; void readIconName() { // Соединяемся с серером if (client.connect(server, 80)) { // И посылаем запрос с тем, что хотим получить страницу // на определенном адресе client.println("GET /data/2.5/weather?q=Moscow&mode=xml&units=metric HTTP/1.1"); client.print("Host: "); client.println(server); client.println("Connection: close"); client.println(); // Ждем, пока придет ответ while (!client.available()) ; char symbol; int i = 0, j = 0; // Пока есть ответ... while (client.available()) { // Считываем принятый байт symbol = client.read(); // Для отладки пишем его в консоль Serial.print(symbol); // Сравниваем его с маской if (symbol == token[i] || token[i] == '?') { // Если сравнение успешно, то проверяем, не дошли ли мы до // цифр в названии файла if ((token[i] == '?') && (j < 2)) { imageId[j] = symbol; ++j; } ++i; } else { // Если сравнение не удалось, то сбрасываем счетчик i = 0; j = 0; } } Serial.println(""); } // останавливаем клиент client.stop(); } void readIconFromFile() { int linesCounter = 0; char byteValue; // OpenWeatherMap возвращает больше иконок, чем у нас есть. // Поэтому происходит небольшое переопределение // Добавляем расширение к имени файла char filename[16]; strcpy(filename, imageId); strcat(filename, ".pbm"); // Окрываем нужный файл File dataFile = SD.open(filename); if (dataFile) { // Пропускаем ненужное. Ищем начало четвертой строки while (linesCounter < 3) { byteValue = dataFile.read(); if (byteValue == '\n') ++linesCounter; } // Обнуляем имеющийся массив-изображение for (int i = 0; i < 8; ++i) { iconBytes[i] = 0; } // Считываем данные, выставляя единички в местах массива, // соответствующих единичкам в PBM-файле for (int i = 0; i < 64; ++i) { // Считанный символ - это один бит if (dataFile.read() == '1') iconBytes[i / 8] |= 1 << (i % 8); } } else { // Если чтение не удалось (например, файл не был найден), // ничего не считываем } // Закрываем файл после чтения dataFile.close(); } void hook() { // Во время работы запрашиваем новую погоду 1 раз в 15 минут if (millis() - startTime >= 15UL * 60UL * 1000UL) { readIconName(); readIconFromFile(); startTime = millis(); } // Очищаем экран marix.clear(); // Выводим погоду на индикатор marix.put_pic(0, 0, 8, 8, iconBytes, MULTIPLY, RED + GREEN, TOP_LAYER); } void setup() { // Инициализируем периферию Serial.begin(9600); Ethernet.begin(mac, ip); pinMode(ETHERNET_CHIP_SELECT_PIN, OUTPUT); if (!SD.begin(SD_CHIP_SELECT_PIN)) { Serial.println("Card failed, or not present!"); //Если по какой-то причине не удается начать работу //с картой памяти, то выводим ошибку } startTime = millis(); // После запуска запрашиваем текущую погоду readIconName(); readIconFromFile(); // Запускаем функцию отображения marix.display(hook); } void loop() { }
За получение погоды отвечает следующий код:
//Соединяемся с серером if (client.connect(server, 80)) { client.println("GET /data/2.5/weather?q=Moscow&mode=xml&units=metric HTTP/1.1"); client.print("Host: "); client.println(server); client.println("Connection: close"); client.println(); ... }
Это запрос на получение необходимого файла с сервера. Фактически, после подключения к серверу, Arduino сообщает ему следующее:
«Мне нужен файл по адресу /data/2.5/weather?q=Moscow&mode=xml&units=metric
. Искать его надо на сервере api.openweathermap.org
. После передачи соединение необходимо разорвать.»
Это стандартный GET-запрос по протоколу HTTP. Тому самому протоколу, с которым работает любой браузер. Подробнее о нем можно прочитать в соответствующей статье в Викпедии.
Перед началом работы скачайте с сайта производителя библиотеку для работы с данным индикатором. Также не забудьте добавить ее в среду разработки!
За инициализацию класса отвечает следующий код:
#define N_X 1 #define N_Y 1 #define DATA_PIN 5 #define CLK_PIN 6 #define LATCH_PIN 2 rgb_matrix matrix = rgb_matrix(N_X, N_Y, DATA_PIN, CLK_PIN, LATCH_PIN);
Здесь N_X
и N_Y
— размер холста по вертикали и горизонтали. Чтобы посчитать размер холста в пикселях, необходимо умножить числа N_X
и N_Y
на 8.
Необходимо учитывать, что холст может быть больше, чем размер одного индикатора: можно запросто соединить несколько, чтобы получить большую панель, к примеру, для вывода строки текста.
Я думаю, вас удивил тот факт, что функция loop
пустая. Это происходит по той причине, что дисплей использует динамическую индикацию. Поэтому библиотека должна «перехватить» управление и выводить что-либо на индикатор постоянно.
Для этого используется следующий код:
matrix.display(hook);
Где hook — имя функции, в которой производится обновление информации на дисплее.
Обновление информации идет в двух строчках функции hook
.
//Очищаем экран matrix.clear(); //Выводим погоду на индикатор matrix.put_pic(0, 0, 8, 8, iconBytes, MULTIPLY, RED + GREEN, TOP_LAYER);
Пройдемся по параметрам функции put_pic
.
0
— координата X левого верхнего угла изображения0
— координата Y левого верхнего угла изображения8
— размер изображения по оси X в пикселах8
— размер изображения по оси Y в пикселахiconBytes
— собственно, картинкаMULTIPLY
— способ вывода изображенияRED + GREEN
— цвет выводаTOP_LAYER
— слой выводаТут следует остановиться на трех последних параметрах.
MULTIPLY
и COVER
. В первом при выводе происходит умножение того цвета, который был, на тот, что будет. А во втором — картинка выведется поверх того, что было.RED
, GREEN
, BLUE
.TOP_LAYER
и имеет нулевой номер. Слои здесь — аналог слоев в Adobe Photoshop. Если у нас есть какое-нибудь сложное изображение, то его можно разбить на несколько частей и поместитить их в разные слои. Тогда в случае небольшого изменения подобного изображения, можно будет изменить лишь содержимое какого-нибудь одного слоя.В данной статье было показано, как запросить нужную страницу с сервера и как найти на ней нужные данные.
А еще то, как подготовить, считать с карты памяти и вывести на светодиодную матрицу картинку.
Я думаю, теперь не будет непосильной задачей сделать, к примеру, отображение текущей дорожной обстановки с Яндекс.Пробок или индикацию количества непрочитанных писем в почтовом ящике.