Как стать автором
Обновить

Кроссплатформенная разработка погодной станции для Raspberry Pi

Время на прочтение 6 мин
Количество просмотров 25K
Как известно, что ни делай под Raspberry Pi, получится либо медиаплеер, либо метеостанция. Постигла эта участь и меня — когда после очередного ливня датчик метеостанции залило, настала пора или купить новую, или сделать самому.

От метеостанции нужны были следующие функции:

  • отображение температуры
  • отображение графика атмосферного давления
  • прогноз дождя
  • синхронизация времени (в случае обычной метеостанции, по DCF77, если уж на устройстве есть часы, они должны показывать точное время)

Из покупных, по сочетанию «дизайн-цена-функции» не понравилась ни одна — либо нет одного, либо другого, либо слишком громоздко и дорого. В итоге решено было задействовать Raspberry Pi с TFT-экраном, и сделать те функции, которые нужны.

Получилось примерно так:



Подробности реализации и готовый проект под катом.

Получение данных погоды


Первое, с чем нужно было определиться — это получение данных погоды. Здесь есть 2 варианта, либо использовать свои датчики, либо брать погоду из интернета. Первое интереснее, но есть несколько «но»:

  • Сделать датчик «абы как» несложно, но сделать датчик хорошо, чтобы он например год работал от одного комплекта батареек, задача уже не столь тривиальная. Есть конечно сейчас и малопотребляющие процы, и радиомодули, но потратить на это месяц было бы лень.
  • Нужно заморачиваться с корпусом, влагозащитой и прочими мелочами (3д-принтера у меня нет).
  • Балкон выходит на солнечную сторону, так что погрешность измерения температуры в первую половину дня была бы слишком большой.

Альтернативным вариантом была покупка готового метеомодуля с датчиками для Raspberry Pi.
Увы, поиск показал что в продаже есть всего 2 варианта:

Raspberry Pi sense hat

Плата имеет «на борту» термометр, барометр, датчик влажности, гироскоп и акселерометр — но чем думали разработчики, ставя такой «экран» 8х8 светодиодов, непонятно — ничего внятного на нем вывести нельзя. Желаю разработчикам этого модуля всю жизнь UI под матрицу 8х8 писать :)

Raspberry Pi weather hat

Ничего кроме светодиодов здесь нет вообще.

В общем, как ни странно, но нормального готового шилда для метеостанции с хорошим экраном и хорошим набором датчиков так никто и не сделал. Краудсорсеры, ау — рыночная ниша пропадает :)

В итоге, не паримся и делаем по-простому — берем погоду из Интернета и выводим на обычный TFT. Как подсказал гугл, самое развитое API сейчас у https://openweathermap.org/api, его и будем использовать.

В комментариях был вопрос про точность интернет-данных. Как можно видеть в коде ниже, для вывода температуры и атмосферного давления используется запрос на получение текущих (current weather data), а не прогнозируемых данных. Их точность можно считать вполне достаточной для бытовых целей, более того, скорее всего она даже выше, чем точность уличного термометра/датчика, размещенного за окном или на балконе.

Регистрация


Для получения данных погоды с openweathermap нужен ключ, его можно получить бесплатно, зарегистрировавшись на вышеупомянутом сайте. Ключ выглядит примерно так «dadef5765xxxxxxxxxxxxxx6dc8». Большинство функций доступны бесплатно, платные API нам не понадобятся. Для бесплатных функций есть ограничение на 60 запросов в минуту, нам этого достаточно.

Чтение данных


Чтение данных весьма просто благодаря библиотеке pyowm.

Получение погоды на данный момент (Python):

import pyowm

owm = pyowm.OWM(apiKey)
observation = owm.weather_at_coords(lat, lon)
w = observation.get_weather()
dtRef = w.get_reference_time(timeformat='date')
t = w.get_temperature('celsius')
temperatureVal = int(t['temp'])

p = w.get_pressure()
pVal = int(p['press'])

Получение прогноза погоды для отображения осадков:

fc = owm.three_hours_forecast_at_coords(lat, lon)
rain = fc.will_have_rain()
snow = fc.will_have_snow()
rains = fc.when_rain()

На выходе получаем массив данных со списком дождей и их интенсивностью. Несмотря на название функции three_hours_forecast_at_coords, дожди прописаны на 2-3 дня вперед.

Можно использовать GET-запросы напрямую, например так. Это может пригодиться, например при портировании кода на MicroPython под ESP.

Получение координат пользователя


Как можно видеть выше, для получения данных нужны широта и долгота. Получение координат также весьма просто, и делается в 3 строчки кода:

import geocoder

g = geocoder.ip('me')
lat = g.latlng[0]
lon = g.latlng[1]

UI


Собственно, самая сложная часть. На Raspberry Pi используется TFT-дисплей от Adafruit, поддерживающий систему команд ILI9340. Библиотеки под него найти несложно, однако отлаживать код на Raspberry Pi не очень удобно. В итоге было принято решение написать высокоуровневый набор контролов, которых нужно было всего 3 — изображения, текст и линии. При запуске на Raspberry Pi контрол будет рисовать себя на TFT, при запуске на десктопе будет использоваться встроенная в Python библиотека tkinter. В итоге, код будет работать везде — и на Raspberry Pi, и на Windows, и на OSX.

Код одного контрола выглядит примерно так:

class UIImage:
  def __init__(self, image = None, x = 0, y = 0, cId = 0):
    self.x = x
    self.y = y
    self.width = 0
    self.height = 0
    self.cId = cId
    self.tkID = None
    self.tftImage = None
    self.tkImage = None
    self.useTk = utils.isRaspberryPi() is False
    if image is not None:
        self.setImage(image)

  def setImage(self, image):
    width, height = image.size
    if self.useTk:
        self.tkImage = ImageTk.PhotoImage(image)
    self.tftImage = image
    self.width  = width
    self.height = height

  def draw(self, canvas = None, tft = None):
    if tft != None:
        tft.draw_img(self.tftImage, self.x, self.y, self.width, self.height)
    elif canvas != None and self.tkImage != None:
        if self.tkID == None or len(canvas.find_withtag(self.tkID)) == 0:
            self.tkID = canvas.create_image(self.x, self.y, image=self.tkImage , anchor=tkinter.NW)
        else:
            canvas.itemconfigure(self.tkID, image=self.tkImage)

Класс «FakeTFT» создает обычное окно программы:

class FakeTFT:
    def __init__(self):
        self.tkRoot = tkinter.Tk()
        self.tkRoot.geometry("500x300")

        self.screenFrame = tkinter.Frame(self.tkRoot, width=330, height=250, bg="lightgray")
        self.screenFrame.place(x=250 - 330 / 2, y=5)

        self.tkScreenCanvas = tkinter.Canvas(self.tkRoot, bg = 'white', width = 320, height = 240, highlightthickness=0)
        self.tkScreenCanvas.focus_set()
        self.tkScreenCanvas.place(x=250 - 320 / 2, y=10)

        self.controls = []

    def draw(self):
          for c in self.controls:
              c.draw(self.tkScreenCanvas)

Класс «LCDTFT» использует «настоящий» дисплей (фрагмент кода):

class LCDTFT:
    def __init__(self, spidev, dc_pin, rst_pin=0, led_pin=0, spi_speed=16000000):
        # CE is 0 or 1 for RPI, but is actual CE pin for virtGPIO
        # RST pin.  0  means soft reset (but reset pin still needs holding high (3V)
        # LED pin, may be tied to 3V (abt 14mA) or used on a 3V logic pin (abt 7mA)
        # and this object needs to be told the GPIO and SPIDEV objects to talk to
        global GPIO
        self.SPI = spidev
        self.SPI.open(0, 0)
        self.SPI.max_speed_hz = spi_speed

        self.RST = rst_pin
        self.DC = dc_pin
        self.LED = led_pin

        self.controls = []

    def draw(self):
          for c in self.controls:
              c.draw(tft = self)

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

def lcdInit():
  if utils.isRaspberryPi():
      GPIO.setwarnings(False)
      GPIO.setmode(GPIO.BCM)
      
      DC =  25
      LED = 18
      RST = 0
      return LCDTFT(spidev.SpiDev(), DC, RST, LED)
  else:
      return FakeTFT()

Все это позволяет полностью абстрагироваться от «железа», и писать код типа такого:

self.labelPressure = libTFT.UILabel("Pressure", 18,126, textColor=self.tft.BLACK, backColor=self.tft.WHITE, fontS = 7)
self.tft.controls.append(self.labelPressure)
self.labelRain = libTFT.UILabel("Rain", 270,126, textColor=self.tft.BLUE, backColor=self.tft.WHITE, fontS = 7)
self.tft.controls.append(self.labelRain)

Собственно UI выглядит так:



На экране отображаются текущая температура, график атмосферного давления за сегодняшний день (в следующей версии будет добавлен график температуры), также в случае прогноза дождя, его время отмечается на графике вертикальной синей чертой (на данной картинке дождей нет). Также выводятся время последнего обновления данных и IP-адрес, если понадобится подключиться к устройству.

Желающие ознакомиться с исходником подробнее, могут посмотреть его на guthub.

Установка на Raspberry Pi


Для тех, кто не хочет заморачиваться описанным выше, короткая инструкция по установке под спойлером.

Инструкция
— Скачиваем исходники с github:
git clone github.com/dmitryelj/RPi-Weather-Station.git

— Если не установлен Python3, ставим:
sudo apt-get install python3

— Ставим дополнительные библиотеки (они нужны для работы с дисплеем):
sudo pip3 install numpy pillow spidev

— Добавляем в автозапуск (sudo nano /etc/rc.local)

python3 /home/pi/Documents/RPi-Weather-Station/weather.py &

— Пробуем запустить

python3 weather.py

Если все работает, то перезагружаемся (sudo reboot) и пользуемся.

В плане добавить еще что-нибудь полезное, например отображение карты облачности, API на openweathermap для этого есть.

Продолжение следует.
Теги:
Хабы:
+20
Комментарии 58
Комментарии Комментарии 58

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн
PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн