Pull to refresh

Лампа, показывающая прогноз погоды

Reading time 13 min
Views 59K
Многие из нас, прежде чем выйти из дома утром, проверяют прогноз погоды на предстоящий день. Я всегда использовал для этого свой смартфон и, однажды, задумался, а почему бы не сделать этот процесс более простым и удобным. Так, в голову пришла идея создания комнатной лампы, которая бы умела показывать прогноз погоды в моей местности, а так же предупреждать о возможных осадках и скорости ветра.



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

Демонстрация работы


Лампа умеет показывать прогноз погоды на 14 часов вперед. Технически внутри лампы есть 14 горизонтальных уровней (RGB LED полосок по 20 светодиодов в каждой). Первый уровень снизу, это погода которая будет в начале следующего часа. Каждый следующий уровень это плюс 1 час. Движение на каждом уровне по горизонтали — это скорость ветра. Так же есть эффект дождя — плавно мигающие всеми цветами части в начале и конце каждого уровня.

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



Вы можете посмотреть еще видео работы лампы без крышки и видео с демонстрацией отображения осадков (светящиеся всеми цветами края полосок).

Требования и проектирование


Для начала попробуем сформулировать требования:

  • Постоянное подключение к интернету. Нам понадобится получать прогноз погоды из интернета
  • Автономность. Лампа не должна зависеть от других устройств
  • Возможность отображения различных погодных данных (температура, дождь, гроза, ветер)

После некоторых раздумий я решил остановится на прямоугольной лампе в которой будет 12 горизонтальных уровней с 20 светодиодами в каждом. Это позволит нам отобразить прогноз погоды на 12 часов вперед. Цвет каждого уровня зависит от температуры воздуха в это время. При этом, каждый уровень будет иметь достаточное количество светодиодов для отображения различных эффектов, таких как дождь, ветер или гроза. Уже потом количество уровней было увеличено до 14, так как оставались лишние светодиоды.

Железо


В первую очередь, необходимо определиться с платформой: микроконтроллер реального времени типа Arduino или же полноценный компьютер, такой как Raspberry Pi с операционной системой на борту. У каждого из этих вариантов есть свои плюсы и минусы. На первый взгляд, Arduino идеально подходит для данного проекта, хотя бы потому, что нам не нужно будет ждать 10 секунд загрузки ОС на Raspberry Pi. Но даже если использовать Arduino, мгновенного холодного старта не получится — все равно будет задержка на инициализацию wifi шилда и запрос погоды на сервере. Так же меня немного смущал вопрос по поводу одновременной работы wifi шилда (во время запроса прогноза на сервер) и работы светодиодной ленты.

В свою очередь, если использовать Raspberry Pi, мы имеем только один недостаток — время загрузки.

Было принято решение использовать Raspberry Pi с WiFi USB донглом EdiMax. Данный донгл был мной использован на других проектах и достаточно хорошо себя зарекомендовал.

Далее нужно найти подходящие источники света. В грубой прикидке минимально нам нужно порядка 240 светодиодов (12 уровней по 20 светодиодов). Вариант при котором их всех придется паять по одному даже не рассматривался. Выбор у нас не большой: либо светодиодные панели, либо светодиодная лента. Панели отлично подойдут тем, кто хочет не большую по размерам лампу без различных искривлений на поверхности. Я же остановился на светодиодной ленте, так как хотел сделать лампу средних размеров.

Таким образом, была заказана RGB светодиодная лента на 2 метра с плотностью пикселей 144 штуки на метр. Данная лента имеет адресные светодиоды (digitally-addressable type of LED strip), это значит что мы можем сформировать сигнал таким образом, что каждый светодиод получит свои данные и отобразит тот цвет, который он должен. За это отвечает микросхема WS2811, которая находится в каждом светодиоде на ленте. Так как всего в ленте получается 288 светодиодов, было решено использовать их по максимуму и сделать 14 уровней по 20 светодиодов)

Следует отметить, что Raspberry Pi выдает только 3,3 вольта на своих GPIO портах, но для ленты нам нужен управляющий сигнал в 5 вольт. Таким образом, нам понадобится преобразователь напряжения (level converter chip). Я использовал 74AHCT125.

Схема подключения (была взята из Adafruit туториала):



В ближайшем магазине была присмотрена и куплена лампа донор с размерами 60 на 20 см. Лампа покупалась с расчетом, что нам нужно будет разместить там блок питания для светодиодной ленты. Так как у нас получилось 280 RGB светодиодов + Raspberry Pi, был заказан блок на 5 вольт 10 ампер в достаточно компактном корпусе.

Настал черед все это разместить в лампе. С блоком питания, Raspberry Pi все было ясно. Чего не скажешь о том, как же закрепить 14 отрезков со светодиодами, которые предварительно необходимо было спаять между собой. Светодиоды должны были быть на определенном расстоянии от матовой крышки, иначе бы они были бы видны и свет был бы слишком резким.

Первоначальная идея была использовать алюминиевые полоски и уже к ним приклеить кусочки ленты. Но сделав один фрейм, я быстро понял что это займет слишком много времени. После этого я решил распечатать фреймы на 3D принтере. Если есть доступ к лазерному гравировщику, вы сделаете это еще быстрее. На крайний случай можно сделать все руками — вырезав из дерева или картона (после это многократно склеив слои).

Печать фреймов:



Первые тесты ленты на рамках:



Итого мы получили все необходимые компоненты и настал черед сборки. Светодиодная лента была разрезана на куски по 20 светодиодов. Куски были спаяны между собой и приклеены к фреймам. Фреймы в свою очередь были приклеены к корпусу. Блок питания, вся разводка и Raspberry Pi были помещены в пространство между фреймами и корпусом.

Процесс сборки лампы:



Результат (В большем разрешении):



Сервис для получения прогноза погоды


Для работы лампы необходим сервис для получения прогноза погоды. Существует большое количество бесплатных и условно бесплатных сервисов для этого. Например openweathermap.org или forecast.io. У всех из них есть свои ограничения или какие-то определенные особенности.

Одним из главных моих критериев было умение получать почасовой прогноз погоды на следующие 12+ часов. К сожалению, openweathermap может возвращать прогноз погоды только по 3 часа в бесплатном режиме. Так же мне не понравилась скорость работы данного сервиса, хотя это было совсем не критично учитывая, что обновлять прогноз погоды мы собираемся не чаще чем раз в пол часа.

В то же время, не совсем бесплатный forecast.io порадовал скоростью работы и детализацией данных. Он позволял получить одним запросом все данные, которые мне были необходимы (температура, скорость ветра и количество осадков) на 12 часов вперед и даже больше. Количество запросов, которые вы можете сделать лимитированно 1000 в сутки в бесплатном режиме, но это вполне укладывалось в мои требования. В итоге я выбрал данный ресурс, честно говоря, просто положившись на свою интуицию.

Предстояло решить как мы будем получать данные: через промежуточный ресурс или напрямую с forecast.io. JSON файл, который возвращал сервис forecast.io, весил порядка 40 килобайт для мой локации, что показалось мне избыточным. Мне нужны были только 3 значения за каждый из 12 часов. В итоге я решил создать свой небольшой сервис по 2 причинам — минимизировать объем пересылаемых данных в лампу и обеспечить будущую расширяемость, если мне придется в будущем поменять источник данных или поменять провайдера. С учетом того, что нам нужны только 3 значения (температура, скорость ветра и количество осадков) для каждого часа, всего требуется передать 168 байт (14 * 3 * размер int = 4). Так же мой сервис будет позволять задавать координаты местности и значения минимальных и максимальных температур для заданной местности, чтобы избежать хранения данной информации на стороне Raspberry Pi.

Я написал Java сервлет для работы с forecast.io, который умеет кэшировать значения между запросами и в случае слишком частых запросов возвращает значение с кэша (для того, чтобы не превысить лимит в 1000 бесплатных запросов в сутки). Новый прогноз мы запрашиваем только один раз в 5 минут. Координаты местности а так же API ключ для forecast.io сервлет берет из системных проперти, таким образом, если нам нужно поменять местность — мы можем это сделать извне веб приложения.

Код сервлета
public class ForecastServlet extends HttpServlet {
    private static final String API_KEY = System.getenv("AL_API_KEY");
    private static final int REQUEST_PERIOD = 5 * 60 * 1000;
    private static final int START_HOUR = 0;
    private static final int END_HOUR = 14;
    private static final int DATA_SIZE = 3 * 4 * (END_HOUR - START_HOUR);
    private static final int TEMP_MULTIPLY = 100;
    private static final int WIND_MULTIPLY = 100;
    private static final int PRECIP_MULTIPLY = 1000;

    private final String mutex = "";
    private final ByteArrayOutputStream data = new ByteArrayOutputStream(DATA_SIZE);
    private long lastRequestTime;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException {
        synchronized (mutex) {

            if ((System.currentTimeMillis() - lastRequestTime) > REQUEST_PERIOD) {
                try {
                    updateForecast();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            response.setHeader("Content-Type", "application/octet-stream");
            response.setHeader("Content-Length", "" + data.size());
            response.getOutputStream().write(data.toByteArray());
            response.getOutputStream().flush();
            response.getOutputStream().close();
            lastRequestTime = System.currentTimeMillis();
        }
    }

    private void updateForecast() throws IOException {
        int maxTemp = Integer.valueOf(System.getenv("AL_MAX_TEMP")) * TEMP_MULTIPLY;
        int minTemp = Integer.valueOf(System.getenv("AL_MIN_TEMP")) * TEMP_MULTIPLY;

        BufferedReader reader = null;
        try {
            String urlTemplate = "https://api.forecast.io/forecast/%s/%s,%s";
            URL url = new URL(String.format(urlTemplate, API_KEY, System.getenv("AL_LAT"), System.getenv("AL_LON")));
            InputStreamReader streamReader = new InputStreamReader(url.openStream());
            reader = new BufferedReader(streamReader);

            JSONParser jsonParser = new JSONParser();
            try {
                JSONObject jsonObject = (JSONObject) jsonParser.parse(reader);
                JSONArray hourly = (JSONArray) ((JSONObject) jsonObject.get("hourly")).get("data");

                for (int i = START_HOUR; i < END_HOUR; i++) {
                    JSONObject hour = (JSONObject) hourly.get(i);

                    int temperature = safeIntFromJson(hour, "apparentTemperature", TEMP_MULTIPLY);

                    if (temperature > maxTemp) {
                        temperature = maxTemp;
                    } else if (temperature < minTemp) {
                        temperature = minTemp;
                    } else {
                        float tempFloat = (float) 100 / (maxTemp - minTemp) * (temperature - minTemp);
                        temperature = (int) (tempFloat * TEMP_MULTIPLY);
                    }

                    int wind = safeIntFromJson(hour, "windSpeed", WIND_MULTIPLY);
                    int precip = safeIntFromJson(hour, "precipIntensity", PRECIP_MULTIPLY);

                    data.write(intToBytes(temperature));
                    data.write(intToBytes(wind));
                    data.write(intToBytes(precip));
                }

            } catch (ParseException e) {
                e.printStackTrace();
            }
        } finally {
            try {
                if (reader != null) reader.close();
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        }
    }

    private byte[] intToBytes(int value) {
        return ByteBuffer.allocate(4).putInt(value).array();
    }

    private int safeIntFromJson(final JSONObject data,
                                final String dataKey,
                                final int multiply) throws IOException {
        Object jsonAttrValue = data.get(dataKey);
        if (jsonAttrValue instanceof Long) {
            return (int) ((Long) jsonAttrValue * multiply);
        } else {
            return (int) ((Double) jsonAttrValue * multiply);
        }
    }
}

Необходимо пояснить, что значат 5 проперти, значения которых мы запрашиваем в рантайм:

AL_API_KEY — Секретный ключ разработчика forecast.io
AL_LAT, AL_LON — Координаты местности
AL_MAX_TEMP, AL_MIN_TEMP — Значения минимальных и максимальных температур для данной местности. Это необходимо, чтобы не тратить напрасно некоторые участки в используемом цветовом диапазоне: допустим в моей местности (штат Техас, США) — температура никогда не опускается ниже нуля, и мне хотелось бы, чтобы фиолетовый цвет (самый нижний в нашей палитре) как раз обозначал 0 а не -25, как можно было бы выставить для Москвы. Таким образом, наш сервис не возвращает реальную температуру — он возвращает одну сотую процента между AL_MIN_TEMP & AL_MAX_TEMP.

Исходный код веб-приложения вместе с файлом сборки maven доступен в репозитории github.com/manusovich/aladdin-service

Далее нам нужен любой хостинг для нашего Java веб приложения. Я воспользовался своим любимым heroku, но вы можете использовать любой другой. В репозитории уже находится файл, необходимый для запуска приложения в среде heroku с именем Procfile.

Итого, если мы используем heroku, все что нам нужно сделать это:

  • Создать новое приложение
  • Определить 3 новых системных проперти
  • Связать его с нашим гит репозиторием
  • Развернуть приложение. Для этого необходимо выполнить Manual Deploy, при этом весь код будет автоматически загружен из github репозитория, скомпилирован и запущен

Теперь наш сервлет может быть выполнен путем открытия в браузере ссылки https://aladdin-service.herokuapp.com/forecast. При этом будет возвращен файл с прогнозом погоды (размером 168 байт) для заданной местности (проперти для приложения в heroku)

Программное обеспечение на стороне лампы


В первую очередь необходимо определиться, как мы будем посылать сигнал на нашу светодиодную ленту. В ленте используется микросхема WS2811 для управления сведодиодом. После непродолжительных поисков я наткнулся на туториал от Adafruit — learn.adafruit.com/neopixels-on-raspberry-pi, где я нашел упоминание о библиотеке rpi_ws281x, которая как раз позволяет формировать сигнал для ленты на базе WS281x микросхем.

Я сделал форк библиотеки в свой репозиторий и добавил необходимый код в main.c (см. ниже в секции контроллер лампы), чтобы упростить до минимума разработку.

Следует сделать небольшое отступление и рассказать, как я обычно разрабатываю код для своих проектов на базе Raspberry Pi. Редактировать код через ssh я нашел совсем не удобным. Копировать код постоянно через ssh тоже. Поэтому я просто создаю GitHub репозиторий, заливаю весь код туда и использую свою любимую IDE для разработки. На стороне Raspberry Pi я создаю шелл скрипт, который раз в 10 секунд пытается получить изменения из репозитория. Если они есть, то скрипт останавливает выполнение программы, скачивает обновления, компилирует все и запускает программу. Скрипт вешается на автозагрузку. Это позволяет разрабатывать код удаленно и в то же время убыстряет процесс проверки изменений на девайсе. Но при этом нагружает wifi сеть. Когда разработка ПО завершена, я делаю период обновления больше — например 60 минут и оставляю это навсегда в таком состоянии.

Алгоритм при этом получается следующий:

  • Запросить изменения в git
  • Если есть изменения в репрозитории то
    • Обновить код
    • Скомпилировать код
    • Если компиляция прошла успешно то
      • Остановить работающее приложение
      • Запустить новое приложение

Настройка RaspberryPi


  • В первую очередь необходимо настроить Wifi
  • После этого нам нужно склонировать репозиторий в директорию /home/pi/rpi_ws281x (выполнить в директории /home/pi):

    git clone https://github.com/manusovich/rpi_ws281x

    Шелл скрипт /home/pi/rpi_ws281x/forecast.sh должен быть добавлен в автозагрузку /etc/rc.local:

    sudo sh /home/pi/rpi_ws281x/forecast.sh >> /home/pi/ws281.log &

Данный скрипт обновляет прогноз, запускает приложение, а так же в фоне каждые 10 минут обновляет прогноз погоды и каждые 60 минут проверят репозиторий проекта на изменения. Если там есть изменения, то они забираются из репозитория, компилируются и запускаются.

Код скрипта

#!/bin/bash

echo "Read forecast"
curl https://aladdin-service.herokuapp.com/forecast > /home/pi/rpi_ws281x/forecast
echo "Kill old instance..."
pkill test
echo "Run new instance..."
exec /home/pi/rpi_ws281x/test &
echo "Start pooling for changes"


C=0
while true; do
    C=$((C+1))

    # once per 10 minutes
    if [ $((C%60)) -eq 0 ]
    then
        echo "Update forecast... "
        curl https://aladdin-service.herokuapp.com/forecast > /home/pi/rpi_ws281x/forecast
    fi

    # once per one hour
    if [ $((C%360)) -eq 0 ]
    then
        echo "Check repository... "
        cd /home/pi/rpi_ws281x
        git fetch > build_log.txt 2>&1

        if [ -s build_log.txt ]
        then
            echo "Application code has been changed. Getting changes..."
            cd /home/pi/rpi_ws281x
            git pull
            echo "Bulding application..."
            scons
            echo "Kill old application..."
            pkill test
            echo "Launch new application..."
            exec /home/pi/rpi_ws281x/test &
            echo "Done"
        else
            echo "No changes in the repository ($N)"
        fi
    fi

   sleep 10s
done

Следует пояснить некоторые моменты:

  1. Абсолютные пути — данный скрипт будет запускаться из автозапуска и нам необходимо указать все пути. Таким образом получается что на Raspberry Pi наш репозиторий должен быть склонирован в директорию /home/pi/rpi_ws281x. Если у вас будет другой путь, вам необходимо будет обновить этот шелл скрипт
  2. Данный скрипт должен быть запущен от имени администратора, так как код управления лентой использует прямой доступ к памяти и должен быть запущен от имени администратора

Контроллер лампы


Теперь давайте рассмотрим код по управлению светодиодами на светодиодной ленте. Данный код находится в файле main.c и представляет из себя бесконечный цикл и набор процедур по изменению цвета светодиодов.

main метод программы содержит инициализацию rpi_ws281x библиотеки для работы со светодиодной лентой и запускает бесконечный цикл по отрисовке состояний:

Код main-метода
int main(int argc, char *argv[]) {
    int frames_per_second = 30;
    int ret = 0;

    setup_handlers();
    if (ws2811_init(&ledstring)) {
        return -1;
    }

    long c = 0;
    update_forecast();
    matrix_render_forecast();

    while (1) {
        matrix_fade();
        matrix_render_wind();
        matrix_render_precip(c);
        matrix_render();

        if (ws2811_render(&ledstring)) {
            ret = -1;
            break;
        }

        usleep((useconds_t) (1000000 / frames_per_second));
        c++;

        if (c % (frames_per_second * 60 * 5) == 0) {
            // each 5 minutes update forecast
            update_forecast();
        }
    }
    ws2811_fini(&ledstring);

    return ret;
}

Метод update_forecast читает актуальный прогноз погоды из файл /home/pi/rpi_ws281x/forecast

Метод matrix_render_forecast заполняет матрицу с текущими значениями прогноза погоды. При этом мы используем палитру из 23 цветов взятых с сайта paletton.com:

ws2811_led_t dotcolors[] = {
        0x882D61, 0x6F256F, 0x582A72, 0x4B2D73, 0x403075, 0x343477, 0x2E4272, 0x29506D, 0x226666,
        0x277553, 0x2D882D, 0x609732, 0x7B9F35, 0x91A437, 0xAAAA39, 0xAAA039, 0xAA9739, 0xAA8E39,
        0xAA8439, 0xAA7939, 0xAA6C39, 0xAA5939, 0xAA3939
};

Метод matrix_fade гасит любые колебания цвета от прогнозной температуры.

Метод matrix_render_wind рисует возбуждение, которое передвигается по горизонтали вперед и назад со скоростью которая равна скорости ветра * на коффициент.

Метод matrix_render_precip отрисовывает осадки по краям уровней. Ему необходим общий счетчик, так как общая скорость обновления 30 кадров в секунду и это оказалось очень быстро для того, чтобы менять цвета. Поэтому мы делаем это только 15 раз в секунду.

Вся отрисовка идет в матрицу XRGB matrix[WIDTH][HEIGHT]. Структура XRGB нам нужна для того, чтобы хранить числа с плавающей точкой вместо целых для цветов. Это позволяет увеличить плавность переходов и непосредственно конвертацию в RGB мы делаем в методе matrix_render

При запуске программа выводит в консоль текущие значение прогноза (температура, ветер и осадки). Следует отметить, что значение температуры — это базисный пункт (одна сотая процента).


pi@raspberrypi ~/rpi_ws281x $ sudo ./test
Temp: 5978, Wind: 953, Precip: 0
Temp: 5847, Wind: 1099, Precip: 0
Temp: 5744, Wind: 1157, Precip: 0
Temp: 5657, Wind: 1267, Precip: 0
Temp: 5612, Wind: 1249, Precip: 1
Temp: 5534, Wind: 1357, Precip: 1
Temp: 5548, Wind: 1359, Precip: 0
Temp: 5605, Wind: 1378, Precip: 0
Temp: 5617, Wind: 1319, Precip: 0
Temp: 5597, Wind: 1281, Precip: 0
Temp: 5644, Wind: 1246, Precip: 0
Temp: 5667, Wind: 1277, Precip: 0

Альтернативные режимы работы


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



Смета и заключение


Блок питания 5 вольт 10 ампер — 25$
2 метра RGB ленты (144 светодиода на метр) — 78$
Raspberry Pi — 30$
Edimax Wifi USB — 8$
3D печать фреймов под светодиодную ленту — 15$ за PLA пластик
Лампа донор — 35$

Итого общая стоимость продукта получилась порядка 200 долларов США при изготовлении в домашних условиях.
Надеюсь, данная статья будем вам полезной. Если у вас возникли какие либо вопросы, смело задавайте в комментариях.
Tags:
Hubs:
+37
Comments 20
Comments Comments 20

Articles