Вывод прогноза погоды из интернета на модуль со светодиодной матрицей

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

Что понадобится

Погода

Собственно, первая проблема, которая встает перед нами — откуда брать погоду? С какого сайта? Я думаю, всем известны такие сайты, как Гисметео или Яндекс.Погода. Но в нашем проекте мы не будем брать у них метеосводку. Мы воспользуемся сайтом, про который, я думаю, вы и не слышали: 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). Изображение в нем представляется в обыкновенном текстовом формате. Вот пример нашей иконки со снегом в этом формате:

snow.pbm
P1
# CREATOR: GIMP PNM Filter Version 1.1
8 8
0011110001000010100000011111111101010000001001010101001000000101

Файл содержит всего 4 строки:

  1. P1 — идентификатор формата. В данном случае — PBM.
  2. Комментарий, содержащий в себе название программы, в которой был создан файл.
  3. 8 8 — количество строк и столбцов
  4. 4-я строка — данные попиксельно: 1 — белый пиксел, 0 — черный.

Можно немного изменить последовательность, чтобы было чуть лучше видно, что это снег. Просто добавим в нужных местах переносы строк. Обратите внимание нарасположение единиц и нулей: они формируют картинку.

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 — данные

В нижнем ряду этого разъёма находятся пины для подключения питания.

Все остальные разъемы нужны для того, чтобы иметь собирать из этих модулей большие матрицы.

Ethernet

Используемый нами 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. Это обусловлено тем, что динамическая индикация, которая реализована матрицей-модулем требует частого и постоянного обмена данными между собой и микроконтроллером.

Код

Мы хотим следующего:

  • Автоматически запрашивать погоду 1 раз в 15 минут
  • Разбирать принятый ответ
  • Считывать нужный файл-изображение с карты памяти
  • Выводить его на экран

Для этого, скетч Arduino может выглядеть так:

wearher.ino
#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.

  1. 0 — координата X левого верхнего угла изображения
  2. 0 — координата Y левого верхнего угла изображения
  3. 8 — размер изображения по оси X в пикселах
  4. 8 — размер изображения по оси Y в пикселах
  5. iconBytes — собственно, картинка
  6. MULTIPLY — способ вывода изображения
  7. RED + GREEN — цвет вывода
  8. TOP_LAYER — слой вывода

Тут следует остановиться на трех последних параметрах.

  • Способ вывода. Может быть одни из двух: MULTIPLY и COVER. В первом при выводе происходит умножение того цвета, который был, на тот, что будет. А во втором — картинка выведется поверх того, что было.
  • Матрица поддерживает три основных цвета: красный, зеленый и синий. А также их комбинации, которые можно получить сложением. В библиотеке цвета объявлены как RED, GREEN, BLUE.
  • Библиотека поддерживает 16 слоев изображений. Но мы используем только первый, который называется TOP_LAYER и имеет нулевой номер. Слои здесь — аналог слоев в Adobe Photoshop. Если у нас есть какое-нибудь сложное изображение, то его можно разбить на несколько частей и поместитить их в разные слои. Тогда в случае небольшого изменения подобного изображения, можно будет изменить лишь содержимое какого-нибудь одного слоя.

Заключение

В данной статье было показано, как запросить нужную страницу с сервера и как найти на ней нужные данные.

А еще то, как подготовить, считать с карты памяти и вывести на светодиодную матрицу картинку.

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