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

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

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

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

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

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


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

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

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

Raspberry Pi weather hat
d294b0eaa70a4c62aa09877b5f4f2c01.jpg
Ничего кроме светодиодов здесь нет вообще.

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

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

Регистрация


Для получения данных погоды с 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 выглядит так:
1c089a825a764aae8983d2993ca7ede1.jpg

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

© Geektimes