Метеостанция на максималках

  • Tutorial

Про метеостанцию на Хабре писали не раз и не два, и наверное не с десяток раз. И вот настало моё время. Решил с вами поделиться своей. 

Постановка задачи

Зачем вообще нужна метеостанция? Сегодня в мире хватает погодных сервисов, в том числе с локальными сводками погоды. Однако помимо контроля внешних параметров мне нужно было получить данные и с датчиков температуры и влажности внутри помещения. Помимо просто информационной составляющей, зная температуру в доме и снаружи, можно, например, управлять котлом или вентиляцией, и поддерживать комфортный микроклимат (погодозависимая автоматика). Кроме того, мне очень хотелось бы отслеживать тенденции и тренды погоды на длительных промежутках времени, например в год-два. То есть, данные нужно где-то хранить.

Из всего того делаем выводы:

  • Нужно хранилище данных (сервер)

  • Датчики будем использовать разные и в разных местах, поэтому проще сделать систему модульной (IoT)

  • Помимо локального сервера данные, хотя бы текущих показаний хорошо бы скидывать в облако

  • Так как мы будем собирать достаточно много данных, можно данными поделиться

Архитектура

Самым простым и популярным решением для метеостанции является Arduino, однако подружить с его домашней сетью - это дополнительные девайсы\шилды, лишние деньги и сложность, а значит - время. Поэтому из коробки проще использовать модуль уже со встроенным Wi-Fi, например ESP8266 (NodeMCU) с подключенными сенсорами. Это достаточно удобно, что один и тот же модуль можно использовать и дома, и за окном. При желании даже можно его использовать в качестве сервера. 

Но почему бы не проставить в центр системы лучше что-то помощнее? Благо у меня пылится без дела Raspberry Pi первой ревизии (но и любая другая подойдёт). Внутренние датчики можно подключить, в принципе через GPIO и к малинке напрямую, но у меня роутер с малинкой в одной комнате установлен, а мониторить нужно другую. Если у вас такой проблемы нет - то можно от одной NodeMCU избавиться. Малинка будет получать данные от датчиков, сохранять их в базе данных и при необходимости отображать. Так же к GPIO Raspberry Pi можно подцепить LoRa - приёмник и получать данные от удалённых за пределами Wi-Fi сети датчиков (и вот они Arduino). Ну, и наконец, малинка будет отправлять данные в облако.

Итого, нам понадобится:

  • Raspberry PI

  • ESP8266 (2шт. + 1шт. опционально)

  • BME280 (2 шт.)

  • Часы реального времени DS1302 (опционально)

  • OLED-дисплей 128х64 на SH1106 (опционально)

  • Датчик дождя на компараторе LM373 (опционально)

  • УФ-датчик GY-VEML6070 (опционально)

  • Raspberry Pi Camera (опционально)

  • Arduino Nano (2 шт., опционально)

  • SX1278 (3 шт., опционально)

  • Магнитный компас с чипом QMC5883L/HMC5883L (опционально)

  • Датчик освещённости (светодиодный) с компаратором LM737 (опционально)

  • Датчики напряжения до 25V (опционально)

  • Датчики тока ACS712 (опционально)

Подключение SX1278 к Raspberry Pi

Для начала подключим к малинке радиомодуль.

Raspberry Pi

SX1278

3.3V

3.3V

GROUND

GROUND

GPIO10

MOSI

GPIO9

MISO

GPIO11

SCK

GPIO8

NSS/ENABLE

GPIO4

DIO0

GPIO22

RST

Соединяем пины Raspberry Pi и SX1278 как на картинке:

Замечание: для разных ревизий Raspberry Pi используются разное количество пинов, а значит и распиновки, смотрите документацию.

По поводу использования LoRa-модулей хочу обратить внимание на несколько моментов:

  • Перед подачей питания на модуль LoRa обязательно убедитесь, что к нему подключена антенна, иначе есть риск, что модуль сгорит!

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

Установка сервера

На Raspberry Pi загружаем Raspberry Pi OS Lite.

Далее устанавливаем статический адрес для нашей малинки:

sudo nano /etc/dhcpcd.conf

Добавляем\правим строки на наш желаемый IP и IP наш роутер

interface eth0 # или wlan0 если малинка подключена по Wi-Fi
static ip_address=192.168.0.4/24 
static routers=192.168.0.1 
static domain_name_servers=192.168.0.1. 8.8.8.8

Теперь включаем удалённый доступ через SSH, SPI (нужен для подключения LoRa), а так же Camera, если планируем её использовать.

sudo raspi-config

Включаем:

  • SSH (если собираемся подключаться по SSH, а не только через клавиатуру)

  • SPI (если собираемся использовать LoRa)

  • Camera (если собираемся использовать камеру)

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

Boot Options -> Console Autologin

Выходим из raspi-config, перезагружаем:

sudo shutdown -r now

Теперь у нас есть удалённый доступ к малинке, можем подключиться через ssh или можем продолжить через клавиатуру.

 Вся логика сервера написана на Python3, поэтому ставим его:

sudo apt-get install python3.7

 Теперь осталось загрузить собственно мой проект H.O.M.E.:

cd ~
git clone https://github.com/wwakabobik/home.git 

В качестве веб-сервера я выбрал flask, на Хабре есть отличная серия статей, поэтому я не буду останавливаться на подробностях при работе с ним.

Копируем контент из папки с сервером:

mkdir web-server
cp -r home/home_server/* /home/pi/web-server/

Устанавливаем все зависимости:

cd web-server
sudo python3.7 -m pip install -r requirements.txt

Создаём базу данных из шаблона:

cat db/schema.sql | sqlite3 flask_db

 Собственно всё, теперь можем запустить сервер:

cd /home/pi/web-server && sudo python3.7 app.py

Но мы же хотим, чтобы сервер запускался при загрузке Raspberry Pi?

Тогда в конце /etc/rc.local, перед exit 0, добавляем вызов bash-скрипта:

/home/pi/flask_startup.sh &

Копируем этот скрипт на место:

cd ~
cp ~/home/bash/flask_startup.sh .

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

cp ~/home/bash/check_health.sh .

Добавляем в планировщик cron:

sudo crontab -e

задание:

1-59/5 * * * * /home/pi/check_health.sh

Немного о софте сервера

За запуск сервера отвечает app.py.

#!/usr/bin/env python3.7

from multiprocessing.pool import ThreadPool

from flask import Flask

from db.db import init_app
from lora_receiver import run_lora


app = Flask(__name__, template_folder='templates')  # firstly, start Flask

# import all routes
import routes.api
import routes.pages
import routes.single_page


if __name__ == '__main__':
    # Start LoRa receiver as subprocess
    pool = ThreadPool(processes=1)
    pool.apply_async(run_lora)
    # Start Flask server
    init_app(app)
    app.run(debug=True, host='0.0.0.0', port='80')
    # Teardown
    pool.terminate()
    pool.join()

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

В остальном архитектура типична для flask’a: все возможные routes вынесены в отдельные файлы, все страницы хранятся в pages, а шаблоны в templates. Логика базы данных лежит в db, статичные файлы (картинки) в static, ну а в camera будем складывать картинки с камеры.

В итоге, текущие показания можно увидеть на dashboard страницах, 

а графики и данные - на отдельных (графики рисует plotly).

Софт LoRa-ресивера

home_server/lora_receiver.py

from time import sleep

import requests
from SX127x.LoRa import *
from SX127x.board_config import BOARD


endpoint = "http://0.0.0.0:80/api/v1"


class LoRaRcvCont(LoRa):
    def __init__(self, verbose=False):
        super(LoRaRcvCont, self).__init__(verbose)
        self.set_mode(MODE.SLEEP)
        self.set_dio_mapping([0] * 6)

    def start(self):
        self.reset_ptr_rx()
        self.set_mode(MODE.RXCONT)
        while True:
            sleep(.5)
            rssi_value = self.get_rssi_value()
            status = self.get_modem_status()
            sys.stdout.flush()

    def on_rx_done(self):
        self.clear_irq_flags(RxDone=1)
        payload = self.read_payload(nocheck=True)
        formatted_payload = bytes(payload).decode("utf-8", 'ignore')
        status = self.send_to_home(formatted_payload)
        if status:
            sleep(1)  # we got the data, force sleep for a while to skip repeats
        self.set_mode(MODE.SLEEP)
        self.reset_ptr_rx()
        self.set_mode(MODE.RXCONT)

    def send_to_home(self, payload):
        if str(payload[:2]) == '0,':
            requests.post(url=f'{endpoint}/add_wind_data', json={'data': payload})
        elif str(payload[:2]) == '1,':
            requests.post(url=f'{endpoint}/add_power_data', json={'data': payload})
        else:
            print("Garbage collected, ignoring")  # debug
            status = 1
        return status


def run_lora():
    BOARD.setup()
    lora = LoRaRcvCont(verbose=False)
    lora.set_mode(MODE.STDBY)
    # Medium Range  Defaults after init are 434.0MHz, Bw = 125 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on 13 dBm
    lora.set_pa_config(pa_select=1)
    assert (lora.get_agc_auto_on() == 1)

    try:
        lora.start()
    finally:
        lora.set_mode(MODE.SLEEP)
        BOARD.teardown()

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

Проверяем в send_to_home что payload[:2] равен ожидаемому коду датчика (я для простоты использую значения «0,» и «1,»), то отсылаем на сервер и спим секунду, чтобы пропустить повторные пакеты.Если нет, продолжаем получать пакеты.

API


Ключевое, что делает 99% времени сервер - это простой. Но в остальные 1% он отдаёт и получает данные, и за это, помимо отображения страниц через веб-интерфейс отвечает API. 

Именно через Flask REST API мы будем посылать или получать данные от сенсоров.

home_server/routes/api.py

@app.route('/api/v1/send_data')
def send_weather_data():
    return send_data()


@app.route('/api/v1/add_weather_data', methods=['POST'])
def store_weather_data():
    if not request.json:
        abort(400)
    timestamp = str(datetime.now())
    unix_timestamp = int(time())
    data = request.json.get('data', "")
    db_data = f'"{timestamp}", {unix_timestamp}, {data}'
    store_weather_data(db_data)
    return jsonify({'data': db_data}), 201

Данные пишутся в лог:

В моём случае, если мы получили данные от датчика (получили POST запрос с верным JSON), то мы их сохраняем в БД. Так же, если мы получили GET запрос на отправку данных (send_data), то данные отправляем данные на облако.

home_server/pages/weather_station/send_data.py

def send_data():
    data = get_last_measurement_pack('0', '1')
    image = take_photo()
    wu_data = prepare_wu_format(data=data)
    response = str(send_data_to_wu(wu_data))
    response += str(send_data_to_pwsw(wu_data))
    response += str(send_data_to_ow(data))
    response += str(send_data_to_nardmon(data))
    send_image_to_wu(image)
    copyfile(image, f'{getcwd()}/camera/image.jpg')
    return response

Ах да, забыл упомянуть камеру. Если мы подключили камеру к Raspberry Pi, то можем отправлять или сохранять изображения погоды за окном. Для этого есть отдельный метод:

home_server/pages/shared/tools.py

from picamera import PiCamera

<...>
camera = PiCamera()
<...>

def take_photo():
    camera.resolution = (1280, 720)  # lower resolution to fit in limits
    camera.start_preview()
    sleep(5)
    image = f'{getcwd()}/camera/image_{int(time())}.jpg'
    camera.capture(image)
    camera.stop_preview()
    return image
  

Внешние датчики

полные скетчи можно найти в home/iot

Самым удобным и простым модулем для любительской метеостанции является модуль BME280, объединяющий в себе термометр, датчик влажности и давления. Подключаем его по I2C к ESP8266:

Прошивать будем через Arduino IDE (как добавить ESP8266   написано, например, в этой статье).

iot/esp8266/weatherstation_in/weatherstation_in.ino

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_BME280.h>
#include <Arduino_JSON.h>

Adafruit_BME280 bme; // use I2C interface
Adafruit_Sensor *bme_temp = bme.getTemperatureSensor();
Adafruit_Sensor *bme_pressure = bme.getPressureSensor();
Adafruit_Sensor *bme_humidity = bme.getHumiditySensor();

// Датчик не сказать, чтобы очень точный, поэтому добавляем корректирующие значения
float correction_temperature = -0.5; 
float correction_pressure = 15;
float correction_humidity = 10; 

// подключаем Wifi
void connect_to_WiFi()
{
   WiFi.mode(WIFI_STA);
   WiFi.begin(wifi_ssid, wifi_password);
   while (WiFi.status() != WL_CONNECTED)
   {
      delay(500);
   }
   Serial.println("WiFi connected");
   Serial.print("IP address: ");
   Serial.println(WiFi.localIP());
   #endif

}

/* <…> */

// собираем данные с датчиков

float get_temperature()
{
    sensors_event_t temp_event, pressure_event, humidity_event;
    bme_temp->getEvent(&temp_event);
    return temp_event.temperature + correction_temperature;

}

/* <…> */

// также точку росы можно вычислить до отправки на сервер, делаем это:
float get_dew_point()

{
    float dew_point;
    float temp = get_temperature();
    float humi = get_humidity();
    dew_point =  (temp - (14.55 + 0.114 * temp) * (1 - (0.01 * humi)) - pow(((2.5 + 0.007 * temp) * (1 - (0.01 * humi))),3) - (15.9 + 0.117 * temp) * pow((1 - (0.01 * humi)), 14));
    return dew_point;
}

/* <…> */

// Форматируем в строку 
String get_csv_data()
{
    String ret_string = DEVICE_ID;
    ret_string += delimiter + String(get_temperature());
    ret_string += delimiter + String(get_humidity());
    ret_string += delimiter + String(get_pressure());
    ret_string += delimiter + String(get_dew_point());
    return ret_string; 
}

// Отправляем через HTTP, упаковав строку в JSON:
void post_data()
{
    check_connection();
    HTTPClient http;    //Declare object of class HTTPClient
    String content = get_csv_data();
    int http_code = 404;
    int retries = 0;
    while (http_code != 201)
    {
        http.begin(api_url); // connect to request destination
        http.addHeader("Content-Type", "application/json");        // set content-type header
        http_code = http.POST("{\"data\": \"" + content +"\"}");   // send the request
        http.end();                                                // close connection
        retries++;
        if (retries > max_retries)
        {
          	Serial.println("Package lost!");
            break;
        }
    }
}

// cобственно, повторяем это время от времени:
void loop()
{
    post_data();
    delay(cooldown);
}

По умолчанию у меня стоит интервал в 5 минут, и я считаю, что DEVICE_ID = "0" – внутренний датчик, а DEVICE_ID = "1" – внешний.

Датчик дождя LM393+YL83
Датчик дождя LM393+YL83

К внешнему датчику можно подключить так же датчики ультрафиолета (
GY-VEML6070) и датчик дождя (на компараторе LM393). YL-83 достаточно игрушечный вариант для реального измерения уровня осадков, по крайней мере без калибровки, но, на какое-то время сгодиться, потому что мне актуальность по уровню осадкам не сильно интересует. Ну, точнее интересует на уровне "на улице дождь" или "сухо". Так же, альтернативно, можно использовать аналоговый датчик ультрафиолета GY-8511, но тогда придётся выбирать между ним и датчиком дождя, так как аналоговый вход на NodeMCU только один. Датчик ультрафиолета можно использовать, например, для оценки эффективности солнечных панелей. Ну и просто показывает дни, когда лучше воспользоваться солнцезащитным кремом во время покоса газона.

Схема подключения к ESP8266 ниже:

Для этих датчиков соответственно добавим три функции:

iot/esp8266/weatherstation_out/weatherstation_out.ino

#include "Adafruit_VEML6070.h"

Adafruit_VEML6070 uv = Adafruit_VEML6070();
#define VEML6070_ADDR_L     (0x38) ///< Low address
RAIN_SENSOR_PIN = A0;

/* <...> */

#ifdef UV_ANALOG_SENSOR
void get_uv_level()
{
    int uv_level = averageAnalogRead(UV_PIN);
    float uv_intensity = mapfloat(uv_level, 0.99, 2.8, 0.0, 15.0);
    return uv_intensity;
}
#endif

#ifdef UV_I2C_SENSOR
void get_uv_level()
{
		return uv.readUV();
}
#endif

#ifdef RAIN_SENSOR
void get_rain_level()
{
    int rain_level = averageAnalogRead(RAIN_SENSOR_PIN);
    return rain_level;
}
#endif

NodeMCU удобно использовать, когда есть устойчивый Wi-Fi в зоне их действия. Конечно, ставить внешние датчики для погодной станции на крыльце - плохая идея, но оборудованная точка, отнесённая пару-тройку метров от дома - то, что нужно, а сигнала роутера в доме хватает за глаза.

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

  • Датчики температуры и влажности устанавливаются на высоте 2 м над землёй в метеорологической будке: это небольшой деревянный или пластиковый ящик (размером приблизительно 40х40х40 см) с белыми, отражающими свет перфорированными или жалюзийными стенками, а также солнцеводозащитным козырьком (крышка будки должна быть герметичной и иметь наклон для стекания осадков с будки).

  • Датчик ветра - на высоте 10-12 м над землёй (именно над землёй, а не на крышах зданий; в исключительном случае разрешается размещение дачтика ветра на крыше одноэтажного дома, так чтобы датчик возвышался над верхним краем крыши не менее чем на 2-3 м, а над поверхностью земли на 10-12 м).

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

  • Датчик атмосферного давления устанавливается в помещении вдали от окон и отопительных приборов. Атмосферное давление зависит от высоты над уровнем моря места, где производится измерение; поэтому требуется калибровка датчика давления перед его использованием. Для правильной установки прибора необходимо воспользоваться показаниями другого барометра или данными ближайшей метеостанции (с учётом разности высот, определённой по подробной топографической карте; 10 м подъёма соответствует уменьшению давления примерно на 1 мм рт.ст. или 1.3 гПа (мБ)).

Как опция, можем добавить ещё одно устройство на ESP8266, которое будет отображать данные с метеостанции. В моём случае - удобно не просто подключить дисплей к внутреннему датчику, а иметь независимое устройство, которое можно расположить где угодно. Для того, чтобы оно что-нибудь ещё делало - добавим к нему часы реального времени.

Затем загрузим на него скетч, который будет отображать текущее время (ЧЧ:ММ) и в бегущей строке данные с метеостанции:

iot/informer/esp8266/informer/informer.ino

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <virtuabotixRTC.h>  // https://ampermarket.kz/files/rtc_virtualbotix.zip


// RTC
virtuabotixRTC myRTC(14, 12, 13);


// OLED
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0);
u8g2_uint_t offset;            // current offset for the scrolling text
u8g2_uint_t width;             // pixel width of the scrolling text (must be lesser than 128 unless U8G2_16BIT is defined
const int string_length = 80;  // maximum count of symbols in marquee
char text[string_length];      // text buffer to scroll

// Wi-Fi
const char* wifi_ssid = "YOUR_SSID";
const char* wifi_password = "YOUR_PASSWORD";

// API
const String ip_address = "YOUR_IP_OF_SERVER";
const String port = "YOUR_PORT";
const String api_endpoint = "/api/v1/add_weather_data";
const String api_url = "http://" + ip_address + ":" + port + api_endpoint;
const int max_retries = 5;  // number of retries to send packet

// Timers and delays
const long data_retrieve_delay = 300000;
const int cycle_delay = 5;
unsigned long last_measurement = 0;


void setup(void) 
{
    Serial.begin(9600);
    init_OLED();
    init_RTC();
}


/* Init functions */
void init_OLED()
{
    u8g2.begin();  
    u8g2.setFont(u8g2_font_inb30_mr); // set the target font to calculate the pixel width
    u8g2.setFontMode(0);              // enable transparent mode, which is faster
}


void init_RTC()
{
    // seconds, minutes, hours, day of the week, day of the month, month, year
    // раскомментируйте при прошивке, заполнив текущую дату и время, затем снова закомментируйте и прошейте ещё раз
    //myRTC.setDS1302Time(30, 03, 22, 5, 19, 2, 2021); // set RTC time
    myRTC.updateTime(); // update of variables for time or accessing the individual elements.
}


""" <...> """

  
String get_data()
{
    check_connection();

    #ifdef DEBUG
    Serial.println("Obtaining data from server");
    #endif
    HTTPClient http;    //Declare object of class HTTPClient
  
    int http_code = 404;
    int retries = 0;
    String payload = "Data retrieve error";
    while (http_code != 200)
    {
        http.begin(api_url);                // connect to request destination
        http_code = http.GET();             // send the request
        String answer = http.getString();   // get response payload
        http.end();                         // close connection    

        retries++;
        if (retries > max_retries)
        {
            break;
            #ifdef DEBUG
            Serial.println("Couldn't get the data!");
            #endif
        }
                
        if (http_code == 200)
        {
            payload = answer;
        }
    }
    return payload;
}


void loop(void) 
{
    // Check that new data is needed to be retrieved from server
    if (((millis() - last_measurement) > data_retrieve_delay) or last_measurement == 0)
    {
        String stext = get_data();
        stext.toCharArray(text, string_length);
        last_measurement = millis();
        width = u8g2.getUTF8Width(text);    // calculate the pixel width of the text
        offset = 0;
    }

    // Update RTC
    myRTC.updateTime(); 

    // Now update OLED
    u8g2_uint_t x;
    u8g2.firstPage();
    do 
    {
        // draw the scrolling text at current offset
        x = offset;
        u8g2.setFont(u8g2_font_inb16_mr);       // set the target font
        do 
        {                                       // repeated drawing of the scrolling text...
            u8g2.drawUTF8(x, 58, text);         // draw the scrolling text
            x += width;                         // add the pixel width of the scrolling text
        } while (x < u8g2.getDisplayWidth());   // draw again until the complete display is filled
    
        u8g2.setFont(u8g2_font_inb30_mr);       // choose big font for clock
        u8g2.setCursor(0, 30);                  // set position of clock
        char buf[8];                            // init bufer to formatted string
        sprintf_P(buf, PSTR("%02d:%02d"), myRTC.hours, myRTC.minutes); // format clock with leading zeros
        u8g2.print(buf);                        // display clock
    } while (u8g2.nextPage());
  
    offset-=2;                       // scroll by two pixels
    if ((u8g2_uint_t)offset < ((u8g2_uint_t) - width))
    {  
        offset = 0;                  // start over again
    }  
    delay(cycle_delay);              // do some small delay
}

В итоге результат работы выглядит так:

Соответственно в Raspberry Pi:

home_server/routes/api.py

@app.route('/api/v1/get_weather_data', methods=['GET'])
def store_wind_data():
    return send_data_to_informer()
  

pages/weather_station/send.data

def send_data_to_informer():
    data_in = get_last_measurement_pack('weather_data', '0', '0')
    data_out = get_last_measurement_pack('weather_data', '0', '1')
    pressure = int((data_in['pressure']+data_out['pressure'])/2)
    formatted_string = f"IN: T={data_in['temperature']}*C, " \
                       f"H={data_in['humidity']}% | " \
                       f"OUT: T={data_out['temperature']}*C, " \
                       f"H={data_out['humidity']}%, " \
                       f"DP={data_out['dew_point']}*C | " \
                       f"P={pressure} mmhg"
    return formatted_string

Радиодатчики

Там, где не дотянуться Wi-Fi, нужно использовать альтернативные варианты передачи данных. В моём случае - это использование LoRa-модулей (в связке, например, с Arduino Nano.

Таких устройств у меня два - это датчик скорости и направления ветра (компас). Пока не буду останавливаться на этом в текущей статье, если будет интерес - напишу отдельно. Второе устройство - это вольтметр и два амперметра, для контроля работы ветряка, зарядки АКБ и потребления.

SX1278

Arduino Nano

3.3V

3.3V

GROUND

GROUND

MOSI

D10

MISO

D11

SCK

D13

NSS/ENABLE

D12

DIO0

D2

RST

D9

И, код, соответственно:

iot/arduino/*_meter/*_meter.ino

// Required includes
#include <SPI.h>
#include <LoRa.h>

// LoRA config
const int LORA_SEND_RETRIES = 5; // сколько раз посылать сообщение
const int LORA_SEND_DELAY = 20;  // задержка между пакетами
const int LORA_POWER = 20;       // мощность передатчика на максимум 
const int LORA_RETRIES = 12;     // сколько раз пытаться инициализировать модуль
const int LORA_DELAY = 500;      // задержка между попыткой инициализации


// Инициализируем модуль
void init_LoRa() 
{
    bool success = false;
    for (int i=0; i < LORA_RETRIES; i++)
    
    {
        if (LoRa.begin(433E6)) // используем 433Мгц
        {
            success = true;
            break;
        }
        delay(LORA_DELAY);
    }
    if (!success)
    {
        #ifdef DEBUG
        Serial.println("LoRa init failed.");
        #endif
        stop(4);
    }
    
    LoRa.setTxPower(LORA_POWER);  // aplify TX power
    #ifdef DEBUG
    Serial.println("LoRa started!");
    #endif  
}
#endif

// Посылаем пакет с данными строкой
void LoRa_send(power_data data)
{
    String packet = DEVICE_ID + "," + String(data.avg_voltage,2) + ",";
    packet += String(data.avg_current,2) + "," + String(data.avg_power,2) + "," +String(data.avg_consumption,2);
    for (int i=0; i < LORA_SEND_RETRIES; i++)
    {
        LoRa.beginPacket();  // just open packet
        LoRa.print(packet);  // send whole data
        LoRa.endPacket();    // end packet
        delay(LORA_SEND_DELAY);
    }  
}

Достаточно просто, не правда ли?

Облачные сервисы

Изначально у меня не было цели делиться данными со сторонними сервисами, но во время отладки появилась мысль о том, что неплохо было бы иметь референсные данные с местных метеостанций, для контроля корректности работы своей. Первым, и с самой большой коллекцией примеров и API оказалась WeatherUnderground.

from wunderground_pws import WUndergroundAPI, units

from secure_data import wu_api_key, wu_reference_station_id

""" ... """

wu_current = wu.current()

""" ... """

wu_humidity=wu_current['observations'][0]['humidity'],
wu_pressure=int(int(wu_current['observations'][0]['metric_si']['pressure'])/1.33),
wu_dew_point=wu_current['observations'][0]['metric_si']['dewpt'],
wu_wind_speed=wu_current['observations'][0]['metric_si']['windSpeed'],
wu_wind_gust=wu_current['observations'][0]['metric_si']['windGust'],
wu_wind_direction=wu_current['observations'][0]['winddir'],
wu_wind_heading=deg_to_heading(int(wu_current['observations'][0]['winddir']))

Однако, если была возможность быстро получать данные, то почему бы ими не поделиться, подумал я? Данные в WU передаются через GET-запрос, поэтому для удобства предварительно подготавливаем данные

def prepare_wu_format(data, timestamp=None):
    payload = f"&dateutc={timestamp}" if timestamp else "&dateutc=now"
    payload += "&action=updateraw"
    payload += "&humidity=" + "{0:.2f}".format(data['humidity'])
    payload += "&tempf=" + str(celsius_to_fahrenheit(data['temperature']))
    payload += "&baromin=" + str(mmhg_to_baromin(data['pressure']))
    payload += "&dewptf=" + str(celsius_to_fahrenheit(data['dew_point']))
    payload += "&heatindex=" + str(celsius_to_fahrenheit(heat_index(temp=data['temperature'], hum=data['humidity'])))
    payload += "&humidex=" + str(celsius_to_fahrenheit(humidex(t=data['temperature'], d=data['dew_point'])))
    payload += "&precip=" + str(data['precip'])
    payload += "&uv" + str(data['uv'])
    return payload

затем отправляем:

import requests

""" ... """

def send_data_to_wu(data):
    wu_url = "https://weatherstation.wunderground.com/weatherstation/updateweatherstation.php?"
    wu_creds = "ID=" + wu_station_id + "&PASSWORD=" + wu_station_pwd
    response = requests.get(f'{wu_url}{wu_creds}{data}')
    return response.content

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

Тут нужно сделать ремарку, что все сервисы, включая WU требуют регистрации и часто получения API ключей. Вся конфиденциальная информация хранится в secure_data.py

# Geo Data
latitude =
longitude =
altitude =
cur_location =

# WEATHER UNDERGROUND DATA
wu_api_key =
wu_station_id =
wu_station_pwd =
wu_reference_station_id =

# OPEN WEATHER DATA
ow_api_key =
ow_station_id =

# PWSWEATHER DATA
pwsw_station_id =
pwsw_api_key =

# NARODMON DATA
narodmon_name = 
narodmon_owner = 
narodmon_mac = 
narodmon_api_key = 

Заполняем значения и продолжаем :)

WeatherUnderground увы, работает на платной основе и полученный мной ключ действует всего лишь год. Поэтому, поискав альтернативы, я наткнулся на PWS Weather.

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

def send_data_to_pwsw(data):
    pwsw_url = "http://www.pwsweather.com/pwsupdate/pwsupdate.php?"
    pwsw_creds = "ID=" + pwsw_station_id + "&PASSWORD=" + pwsw_api_key
    response = requests.get(f'{pwsw_url}{pwsw_creds}{data}')
    return response.content

Этот сервис мне показался более дружелюбным, в нём есть переключатель с имперской на метрическую систему. Плюс красивые графики, прогнозы.

Ещё можно послать данные на OpenWeatherMap. Персональной страницы тут нет, а в ответ на исторические данные мы получим "средние" по больнице данные, но, почему бы и нет? Энтузиастам надо помогать. Для передачи показаний у OWM для PWS (personal weather station) своё API, но что-то я не нашёл готовой обёртки для него на python, поэтому написал свою.

В отличие американских WeatherUnderground и PWS Weather, использующих имперскую систему, разраработчики OpenWeatherMap из Латвии и используют метрическую систему (Си), поэтому для передачи показаний для них не используем конвертеры, а пишем данные сразу из базы данных, которые мы собрали с датчиков.

from openweather_pws import Station

def send_data_to_ow(data):
    pws = Station(api_key=ow_api_key, station_id=ow_station_id)
    response = pws.measurements.set(temperature=data['temperature'], humidity=data['humidity'],
                                    dew_point=data['dew_point'], pressure=data['pressure'],
                                    heat_index=fahrenheit_to_celsius(heat_index(temp=data['temperature'],
                                                                                hum=data['humidity'])),
                                    humidex=humidex(t=data['temperature'], d=data['dew_point']))
    return response

И, наконец, как вариант импортозамещения самый функциональный, позволяющий хранить в том числе данные с закрытых датчиков (например те же данные о температуре внутри дома) - Narodmon.

На сервисе достаточно богатое API, позволяющее не только передавать показания с датчиков, но и собирать информацию по геолокации, управлять самим устройством удалённо, так и социальные фишки вроде "поставить лайк" или отправить сообщение. Особо здорово, что сервис шлёт email'ы в случае проблем (например датчик не вышел на связь час), так и настраиваемые "проблемы" вроде превышения лимита на конкретном датчике. Но, как и в случае OWM я не нашёл полного API-wrapper для python, и опять написал свой. Теперь, чтобы отправить данные с датчиков, зовём:

def send_data_to_nardmon(data):
    nm = Narodmon(mac=narodmon_mac, name=narodmon_name, owner=narodmon_owner,
                  lat=latitude, lon=longitude, alt=altitude)
    temperature = nm.via_json.prepare_sensor_data(id_in="TEMPC", value=data['temperature'])
    pressure = nm.via_json.prepare_sensor_data(id_in="MMHG", value=(data['pressure']))
    humidity = nm.via_json.prepare_sensor_data(id_in="HUM", value=data['humidity'])
    dew_point = nm.via_json.prepare_sensor_data(id_in="DEW", value=data['dew_point'])
    sensors = [temperature, pressure, humidity, dew_point]
    response = nm.via_json.send_short_data(sensors=sensors)
    return response

У данного сервиса есть одна дурацкая отличительная особенность, состоящая в том, что нельзя отправлять данные чаще чем раз в пять минут. Но на практике то ли у нас разные пять минут, то ли сайт подвисает, реально данные отправляются раз в 10-15 минут. Если всё сделано правильно, то увидим данные на сайте.

Немаловажным будет сказать, что для отправки данных следует "дёргать" ручку /api/v1/send_data пустым GET-запросом. Чтобы не изобретать велосипед, просто поручим это делать cron. Добавляем ещё одну строку:

*/5 * * * * /usr/bin/wget -O - -q -t 1 http://0.0.0.0:80/api/v1/send_data

А как же камера?

Пока никак. Сделанные фото можно передавать на WeatherUnderground. Это сделать несложно через ftp

from ftplib import FTP

def send_image_to_wu(image):
    session = FTP('webcam.wunderground.com', wu_cam_id, wu_cam_pwd)
    file = open(image, 'rb')
    session.storbinary('image.jpg', file)
    file.close()
    session.quit()

Однако, даже просто вручную передать данные на WU у меня не получилось ни разу. Судя по форумам техподдержки, данная фича работает плохо и сбоит.

Альтернативной является передача изображения на narodmon.ru,

Собственно, время от времени (раз в полчаса) дёргаем ручку /api/v1/capture_photo (которая зовёт take_photo). Например, будем звать через cron этот bash-скрипт:

#!/bin/bash

PATH_TO_PHOTO=`/usr/bin/wget -O - -q -t 1 http://0.0.0.0/api/v1/capture_photo`
REQUEST='curl -F YOUR_CAM_KEY=@'$PATH_TO_PHOTO' http://narodmon.ru/post'
RESULT=`$REQUEST` >/dev/null 2>&1

На сервисе сразу появится снимок:

Плюс, не забываем время от времени (например раз в семь дней) чистить старые изображения:

#!/bin/bash bash

# Notes:
# This file will remove all files in camera folder older than 7 days, just run in via cron periodically (i.e. daily).
find /home/pi/web-server/camera/ -type f -mtime +7 -name '*.jpg' -execdir rm -- '{}' \;

Что дальше?

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

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Нужна отдельная статья про анемометр и компас?

  • 73,6%Конечно нужна81
  • 10,9%Можно, только покроче12
  • 5,4%Нет, не нужна6
  • 10,0%Не знаю, хочу посмотреть результаты опроса11

Средняя зарплата в IT

120 000 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 6 212 анкет, за 1-ое пол. 2021 года Узнать свою зарплату
Реклама
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее

Комментарии 52

    +2
    ждите в гости пативен управления «Р» радиоконтроля — кто мешал почитать, и купить модули на разрешённый диапазон (SX1276, 868 МГц)?

    ЗЫ: скорее лол, принцип неуловимого Джо спасёт отца метеоролологии, вопрос скорее в том, что потом с этими данными делать дальше — какая-то обработка, прогнозирование на Пытоне и нейросетях
      +1
      Зачем вы вводите людей в заблуждение? Оба диапазона не требуют разрешение для частного использования.

      Диапазон: 433,075…434,79, мощность: 10 мВт
      Описание:
      Неспециализированные (любого назначения) устройства — устройства малого радиуса общего применения, включая устройства дистанционного управления и передачи телеметрии, телеуправления, сигнализации, передачи данных и других подобных передач.
      Источник:
      Приложение 1 к решению ГКРЧ
      от 7 мая 2007 г. № 07-20-03-001

      По поводу что делать с данными я уже писал\говорил: в основном для использования в погодозависимой автоматике для управления климатом в доме (например котлом и насосами отопления), датчики ветра — под ветряк.
    0
    Проголосовал за 1й пункт. Анемометр/компас вам ТСЖ/УК даст на крышу поставить?
      0
      У меня частный дом (участок), лучше не крышу конечно, а штангу использовать.
        0

        А разве УК/ТСЖ у нас согласно закона что-то разрешает или запрещает. Решают собственники жилых помещений в МКД, а УК/ТСЖ — это убрщица, дверник, слесарь, сварщик, сантехник и т. д. и не более.

          0
          Собственникам жилых помещений с УК у нас в 99% всё фиолетово и до лампочки. С ТСЖ — в 80%. ТСЖ — это в первую очередь председатель, бухгалтер, правление, собрание наконец… а потом уже дворник.
        0
        по-хорошему, сделать бы распределённую реализацию ПО — сеть хранения и обработки данных, без каких-либо серверов а-ля narodmon, к которой мог добавляться любой желающий, предоставлять дисковое пространство для данных, GPU для анализа, и точки входа API и web-интерфейса для пользователей если есть белый ip
          0
          На голом энтузиазме — это большая задача, я пока не горю желанием, хотя, возможно в таком проекте поучаствовал бы. Ну, и плюс, с белым ip у меня пока никак.
          +5
          Если на максималках то можно было бы подумать и про датчики радиации, CO2, PM2.5 и какого нибудь «качества воздуха» что бы не понималось под этим абстрактным понятием.
            0
            Ну, можно. Мне оно только не сильно интересно. Что с этими данными делать в доме? Ну, в квартире, городе — может быть. А так, если только как сигнализацию использовать. При желании взял nodemcu/arduino, прилепил к нему датчики, передал показания в эту же систему.
              +2
              Радиационный мониторинг и мониторинг загрязнения воздуха на местности для внешних датчиков. Отслеживание работы вентиляции в помещении для внутренних датчиков.
              Хотя согласен у каждого свои интересы.
                0
                Да, именно. Но, в целом да, можно будет как-нибудь и эти датчики прикупить и подключить любопытства ради.
                +1
                хм. а что вы делаете с данными по давлению, например, или по влажности на улице?

                Данные по PM2.5 например, помогут понять где источник пыли — на улице или в доме и соответственно открыть-закрыть окна-форточки или там просигнализировать о подгорании выкипевшего чайника на газовой плите, или подгорающем контакте в розетке или удлинителе
                  0
                  Давление, конечно внутри дома и снаружи одинаковое, поэтому одного показания хватает. Зачем мне значение давления? Ну, у меня есть метеозависимость, реагирую на понижение давления. Плюс, если очень захочется, по давлению можно научиться предсказывать погоду.

                  А вот влажность для автоматики нужна. Во-первых, зная влажность, можно превентивно подсушить воздух в доме. А во-вторых, что даже важнее: нужна не совсем влажность, а значение точки росы, вычисляемая по температуре и влажности. Дело в том, что в дымоходе, а так же в вентиляции (или, что хуже — на холодном чердаке, через который проходят трубы) может образовываться конденсат (который совсем не нужен в доме), и опять-таки, зная информацию заранее можно заранее включить принудительную вентиляцию.

                  Насчёт форточек, ну, в редких случаях да, согласен, может быть. Но в моём случае это очень редкий сценарий. По поводу предупреждении о задымлении, что вы описываете, конечно, резонно иметь сигнализацию. Но у меня камин и печное отопление есть как альтернатива, и надо будет помучаться с калибровкой.
                    0
                    не совсем понял, как температура и влажность с определяемым по ним точкой росы на улице связана с температурой и влажностью в дымоходе или в вентиляции или даже на холодном чердаке, если он не насквозь продуваем.

                    Пожарный извещатель начнет орать при сильном задымление, а измеритель PM вам покажет утечки дыма, если такие есть. Для печек типа «булерьян» такие утечки получаются при открытии дверцы. Ну или тот же выкипевший чайник должен почуять намного раньше… Если у соседа печное отопление, то может случится ситуация, что дым от него попадает к вам. Если у вас там целый поселок с печным отоплением, то такая ситуация будет чаще.
                      0
                      Холодный чердак потому и холодный, что он «насквозь продуваем». Дымоход и вентиляция — это точки, где воздух\поверхности внешнего и внутреннего воздуха встречаются друг с другом. infotruby.ru/svoimi-rukami/kondensat-i-kak-ot-nego-izbavitsya
                      Тут наверное надо уточнить, что даже хорошо утепленные трубы на чердаке всё равно при определённых условиях (при разнице температур этак в >40 градусов и почти что максимальной влажности на улице (когда идёт днями дождь, например)) будут собирать конденсат. Таких дней в году не много (обычно это смена сезонов осень-зима и весна-лето), но они есть и в эти дни включать дополнительную принудительную вентиляцию — отличное и дешевое решение, чем утеплять еще в пять слоёв чердак или делать его тёплым.
                        0
                        Ну, степень продуваемости чердака можно регулировать — например летом форточки открывать в противоположных торцах, а на зиму закрывать. Совсем хорошая продуваемость грозит еще тем, что может внутрь сугроб намести.

                        А дополнительная принудительная вентиляция — это что? Вентилятор на чердаке, дующий на трубу? А труба кирпичная?
                          0
                          Ну иногда да, проветривают окнами. У меня вентиляционные решетки (без регуляции) в противоположных торцах + щелевая вентиляция через софиты и конёк. Собственно, хочу к решеткам поставить управляемые канальные вентиляторы. Нет, трубы — сэндвич + дополнительная базальтовая обмотка.
                    +1

                    Давление нужно для оценки происходящих метеопроцессов. Разумеется, не само по себе, а как ещё одна точка в дополнение к данным из интернета.

                    +1

                    Можно передавать данные например ссуда https://sensor.community/ru/. Дело хорошее, в вашем случае только добавить датчиков.

                      0
                      Спасибо, изучу.
                    +1

                    Я такой и сделал. Портативная метеостанция с графиками. Часы, со2, радиация, температура, давление, влажность, алкотестер. Может что-то ещё, забыл уже.

                    +1
                    Только вчера кто-то писал в комментах, мол «даже начинаю скучать по гайдам метеостанций на ардуино» и вот!
                      0
                      Стараюсь! *sarcasm* Но тут не только Arduino, тут всё подряд.
                        0
                        Тут наконец-то про анемометр хоть задумались, а то близнецов на ардуино с рюшечками только и плодят. Ну а полный хардкор который должен когда-то взорвать Хабр — это для температуры ОС термометр сопротивления, например Pt100, а анемометр на ультразвуке (4 датчика накрест). Вот тогда можно и на пенсию…
                        0
                        Так то оно так… Но в статье много чего полезного (примеры кода, настройки ОС, работы с датчиками и т.д.) что можно будет использовать в своих целях, а не именно для создания «еще одной метеостанции».
                          0
                          я не спорю и не против, просто забавный факт :)
                        0
                        Тоже сделал очередную метеостанцию на ESP8266 и столкнулся с проблемой — BME280 подогревается иэспишкой (возможно, линейным стабилизатором, который расположен на одной плате с ESP) и выдаёт завышенную температуру. У кого-нибудь есть успешный способ решения этой проблемы? Снижать энергопотребление ESP? Устройство подразумевалось переносным, так что разносить отдельно датчик и ESP на расстояние друг от друга не выход.
                          0
                          А почему не подчключить ds18b20 и не вынести его наружу всем ветрам?
                          Не думаю что точность при погодных измерениях в 0.5 градуса будет недостаточной.
                            +1
                            У меня что-то похожее:

                            Датчик наружу будет банально некрасиво смотреться. Причём у меня ещё даже корпуса нет, но всё равно датчик хорошо так прогревается.
                              0
                              Датчик наружу будет банально некрасиво смотреться.

                              Вам шашечки или ехать?

                                +1
                                Напоминает анекдот про поиск ключей не там где потерял а там где светло)
                                В любом случае датчик придется или выносить наружу или очень хорошо вентилировать забортным воздухом если нужны хоть сколь-нибудь релевантные показания. У меня от автоматики котла, к примеру, датчик вынесен на улицу за стену и если он торчит менее чем на пару сантиметров от стены «в природу» он уже врет на 1-.1.5 градуса вверх, причем зависимость нелинейная от наружной температуры.
                                А когда солнышко выходит то врет еще сильнее (нужен козырек). Но блин, так работает физика, увы.
                                  0

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

                                    0
                                    Так оно не удивительно — бетон отлично греется и делится теплом с улицей. Когда я первый раз для теста на день выкинул датчик на крыльцо бетонное, а затем переложил его на деревянный табурет удивился, что разница в показаниях скакнула аж на 2,5 градуса (вниз — в сторону реальной температуры на улице).
                                      0

                                      Но таки зима, а плита утеплена.
                                      Подозреваю, нижние этажи дают тепло.


                                      Надо в кучку собрать для проверки, вдруг внешний в принципе ушёл в дрейф.

                              +1
                              У меня датчик закреплён снизу платы с ESP, и хорошо проветривается.
                              Наверное можно поставить теплоизолятор между датчиком и ESP, но думаю эффективней будет организация нормального воздухообмена вокруг датчика.
                              image
                                0

                                Так у Вас все как положено, с дискотарелочками)
                                Посмотрите про режим сна esp, многие добиваются автономной работы даже от батареек до месяца…
                                В таком случае esp уж точно не греется ;))

                              0

                              А что не mqtt +nodered +grafana?

                                0
                                Не знаю, что ответить на ваш вопрос. Ну как-то так вышло.
                                Но спасибо за идею, если захочу зарефакторить всё, то попробую.
                                  +2

                                  Также рекомендую mqtt + telegraf + influxdb + grafana

                                0
                                Датчики температуры и влажности воздуха обязательно устанавливаются над естественной поверхностью земли (трава, грунт). Асфальта, бетона, щебня, камня, металла не должно быть.

                                Замечание полезное, но в условиях города это не всегда достижимо.
                                  0
                                  Конечно. Если вы не живёте в городском центре, а всё-таки в типичном спальном районе, то как правило рядом с домом после отмостки есть земля. Собственно метеостанцию очень желательно ставить на штанге (если конечно УК\ТСЖ не против), перекрыв это расстояние с запасом. Но вообще ставить метеостанцию в квартире и говорить о точных метеорологически точных показаниях наверное всё-таки не стоит, потому что здесь важна и высота установки, и воздушные потоки и прочая. Но для любительского устройства, впрочем, как мне кажется достаточно просто убрать влияние утечек тепла и влажности от дома, отодвинув метеостанцию немного (хотя бы на полметра-метр) от окна\поверхности дома\крыши.
                                    +1

                                    Метеоданные, снятые на высоте N-ого этажа могут представлять самостоятельный научный интерес.

                                      0
                                      Да, согласен. Особенно с точки зрения движения воздушных масс и тепловых потоков. Если строить города правильно — то такая информация полезна и должна собираться.
                                  +2
                                  тоже сделал мобильное приложение для отображения метео-данных с устройств play.google.com/store/apps/details?id=info.smartmeteo.smartmeteo
                                  есть интерес это развивать, в том числе, открыть API для мобильного приложения, чтобы данные отображать с других устройств…
                                    +1

                                    Осталось красивый и эргономичный корпус сделать, а так всё классно и интересно рассказано

                                      0
                                      Сам жду — не дождусь когда будет потеплее и можно будет корпуса нарезать, покрасить и установить.
                                      +1

                                      Отлично сделано!
                                      Еще датчик разрядов атмосферных — и вообще красота будет

                                        0
                                        Спасибо.
                                        Вы идеете в виду датчик грозы вроде AS3935? Или что-то другое?

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                      Самое читаемое