Многие из нас, прежде чем выйти из дома утром, проверяют прогноз погоды на предстоящий день. Я всегда использовал для этого свой смартфон и, однажды, задумался, а почему бы не сделать этот процесс более простым и удобным. Так, в голову пришла идея создания комнатной лампы, которая бы умела показывать прогноз погоды в моей местности, а так же предупреждать о возможных осадках и скорости ветра.
Под катом видео и изображения демонстрирующие работу данной лампы и подробная инструкция по её созданию.
Демонстрация работы
Лампа умеет показывать прогноз погоды на 14 часов вперед. Технически внутри лампы есть 14 горизонтальных уровней (RGB LED полосок по 20 светодиодов в каждой). Первый уровень снизу, это погода которая будет в начале следующего часа. Каждый следующий уровень это плюс 1 час. Движение на каждом уровне по горизонтали — это скорость ветра. Так же есть эффект дождя — плавно мигающие всеми цветами части в начале и конце каждого уровня.
Тут следует предупредить, что все фото и видео в этой статье были сделаны путем долгих мучений, так как лампа светит очень сильно, а я не очень опытный фотограф и моя зеркалка просто не хотела снимать то, как это есть на самом деле. Я честно пытался очень много раз но, так и не достиг такой же картинки, как в реальности.
Вы можете посмотреть еще видео работы лампы без крышки и видео с демонстрацией отображения осадков (светящиеся всеми цветами края полосок).
Требования и проектирование
Для начала попробуем сформулировать требования:
- Постоянное подключение к интернету. Нам понадобится получать прогноз погоды из интернета
- Автономность. Лампа не должна зависеть от других устройств
- Возможность отображения различных погодных данных (температура, дождь, гроза, ветер)
После некоторых раздумий я решил остановится на прямоугольной лампе в которой будет 12 горизонтальных уровней с 20 светодиодами в каждом. Это позволит нам отобразить прогноз погоды на 12 часов вперед. Цвет каждого уровня зависит от температуры воздуха в это время. При этом, каждый уровень будет иметь достаточное количество светодиодов для отображения различных эффектов, таких как дождь, ветер или гроза. Уже потом количество уровней было увеличено до 14, так как оставались лишние светодиоды.
Железо
В первую очередь, необходимо определится с платформой: микроконтроллер реального времени типа Arudino или же полноценный компьютер, такой как Raspberry Pi с операционной системой на борту. У каждого из этих вариантов есть свои плюсы и минусы. На первый взгляд, Arudino идеально подходит для данного проекта, хотя бы потому, что нам не нужно будет ждать 10 секунд загрузки ОС на RaspberryPI. Но даже если использовать Arudino, мгновенного холодного старта не получится — все равно будет задержка на инициализацию 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 светодиодов)
Следует отметить, что RaspberryPI выдает только 3,3 вольта на своих GPIO портах, но для ленты нам нужен управляющий сигнал в 5 вольт. Таким образом, нам понадобится преобразователь напряжения (level converter chip). Я использовал 74AHCT125.
Схема подключения (была взята из Adafruit туториала):
В ближайшем магазине была присмотрена и куплена лампа донор с размерами 60 на 20 см. Лампа покупалась с расчетом, что нам нужно будет разместить там блок питания для светодиодной ленты. Так как у нас получилось 280 RGB светодиодов + Raspberry Pi, был заказан блок на 5 вольт 10 ампер в достаточно компактном корпусе.
Настал черед все это разместить в лампе. С блоком питания, RaspberryPi все было ясно. Чего не скажешь о том, как же закрепить 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). Так же мой сервис будет позволять задавать координаты местности и значения минимальных и максимальных температур для заданной местности, что бы избежать хранения данной информации на стороне RaspberryPi.
Я написал 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 и мне хотелось бы что бы фиолетовый цвет (самый нижний в нашей палитре) как раз обозначал 0 а не -25, как можно было бы выставить для Москвы. Таким образом, наш сервис не возвращает реальную температуру — он возвращает одну сотую процента между AL_MIN_TEMP & AL_MAX_TEMP.
Исходный код веб-приложения вместе с файлом сборки maven доступен в репозитории github.com/manusovich/aladdin-service
Далее нам нужен любой
Итого если мы используем 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 (см. ниже в секции контроллер лампы) что бы упростить до минимума разработку.
Следует сделать небольшое отступление и рассказать, как я обычно разрабатываю код для своих проектов на базе RaspberryPi. Редактировать код через ssh я нашел совсем не удобным. Копировать код постоянно через ssh то же. Поэтому я просто создаю GitHub репозиторий, заливаю весь код туда и использую свою любимую IDE для разработки. На стороне RaspberryPi я создаю шелл скрипт который раз в 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
Следует пояснить некоторые моменты:
- Абсолютные пути — данный скрипт будет запускаться из автозапуска и нам необходимо указать все пути. Таким образом получается что на raspberrypi наш репозиторий должен быть склонирован в дирректорию /home/pi/rpi_ws281x. Если у вас будет другой путь, вам необходимо будет обновить этот шелл скрипт
- Данный скрипт должен быть запущен от имени администратора, так как код управления лентой использует прямой доступ к памяти и должен быть запущен от имени администратора
Контроллер лампы
Теперь давайте рассмотрим код по управлению светодиодами на светодиодной ленте. Данный код находится в файле main.c и представляет из себя бесконечный цикл и набор процедур по изменению цвета светодиодов.
main метод программы содержит инициализацию rpi_ws281x библиотеки для работы со светодиодной лентой и запускает бесконечный цикл по отрисовке состояний:
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 долларов США при изготовлении в домашних условиях.
Надеюсь, данная статья будем вам полезной. Если у вас возникли какие либо вопросы, смело задавайте в комментариях.
Автор: manusovich