Я колись вже слідкував за температурою за допомогою Arduino та термі́стора. Тепер роблю це з платою ESP8266 (NodeMCU), датчика BMP280 та програми на MicroPython.
Те, що варто було зробити за один вечір, через вимкнення електроенергії розтягнулося на кілька днів. З іншого боку, саме через відключення світла та опалення я став цікавитися температурою в своїй оселі.
Залізо
Підключення плати BMP280 цілком тривіальне: земля, живлення, шина I²C.
Так, плата NodeMCU в моїй схемі висить між двома маленькими SYB-170. Ну а що ще з нею робити? :) Вона надто широка, щоб поміститися на звичайну маке́тну плату.
Контакт D0 (GPIO16) підключений до RST. Це звичайне рішення для режиму deep sleep у ESP8266. Коли спрацьовує таймер в RTC (в моєму випадку через 1 хвилину), то він подає сигнал на GPIO16 і, відповідно, перезаванта́жує модуль.
Є деяка не дуже типо́ва деталь: контакт D5 (GPIO14) підключений до землі. Це у мене такий типу як «запобі́жник», бо deep sleep досить-таки небезпечний при неакуратному використанні. Тож мій код запускає deep sleep тільки в тому випадку, якщо є з’єднання GPIO14 з землею. Якщо від’єдна́ти відповідний дріт, то після однократного вимірювання код завершить виконання і REPL буде доступним, а значить, можна буде модифікувати програму.
Софт
На внутрішній файловій системі модуля ESP8266 лежать два файли: bmp280.py
, який я взяв прямо з GitHub’у, та мій main.py
:
main.py
import machine
import network
import time
import urequests
from bmp280 import *
config_wifi_essid = '...FIXME...'
config_wifi_password = '...FIXME...'
config_station_id = 'board02'
config_endpoint_url = 'http://192.168.0.12/iot/weather.php'
def wifi_setup():
global sta_if
sta_if = network.WLAN(network.STA_IF)
sta_if.active(True)
sta_if.scan()
sta_if.connect(config_wifi_essid, config_wifi_password)
def wifi_wait_connect(timeout_ms=2500):
while timeout_ms:
if (sta_if.isconnected()):
return True
time.sleep_ms(250)
timeout_ms = timeout_ms - 250
return sta_if.isconnected()
def i2c_setup():
global i2c
i2c = machine.I2C(scl=machine.Pin(5), sda=machine.Pin(4))
def bmp_setup():
global bmp
bmp = BMP280(i2c)
bmp.use_case(BMP280_CASE_WEATHER)
bmp.oversample(BMP280_OS_HIGH)
bmp.temp_os = BMP280_TEMP_OS_8
bmp.press_os = BMP280_PRES_OS_4
bmp.iir = BMP280_IIR_FILTER_2
def bmp_measure():
bmp.force_measure()
result = { 'temperature': bmp.temperature, 'pressure': bmp.pressure }
bmp.sleep()
return result
def send_measurements(bmp_data):
bmp_data['station'] = config_station_id
response = urequests.post(config_endpoint_url, json=bmp_data)
return response.status_code
def go_deepsleep(timeout_ms=60000):
rtc = machine.RTC()
rtc.irq(trigger=rtc.ALARM0, wake=machine.DEEPSLEEP)
rtc.alarm(rtc.ALARM0, timeout_ms)
machine.deepsleep()
# D4 / GPIO2 / LED
d4 = machine.Pin(2, machine.Pin.OUT)
# Turn on the LED
d4.value(0)
# Connect to WiFi
wifi_setup()
if wifi_wait_connect():
i2c_setup()
bmp_setup()
send_measurements(bmp_measure())
# What if there is no WiFi?
# Should I write measurement to some log file?
# TODO
# Turn off the LED
d4.value(1)
# D5 / GPIO14
d5 = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_UP)
# value 1 == pin D5 not connected, use it for debug mode
# value 0 == pin D5 connected to G, so it's safe to use deep sleep
if (d5.value() == 0):
go_deepsleep()
else:
print('Pin D5 is not grounded')
print('Not going to the deep sleep mode')
Все! Ну або майже все. Треба ще якийсь backend.
REST API
Треба кудись ці дані покласти. Наприклад, в базу.
Схема
CREATE TABLE `weather` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
`station` varchar(12) CHARACTER SET ascii DEFAULT NULL,
`temperature` decimal(4,2) DEFAULT NULL,
`pressure` int(6) UNSIGNED DEFAULT NULL,
`humidity` decimal(5,2) UNSIGNED DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Python на мікроконтролері це чудово, але у веб-сервері мені якось приємніше бачити PHP.
weather.php
<?php
declare(strict_types=1);
error_reporting(E_ALL);
ini_set('display_errors', '1');
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
echo '<h1>IoT API</h1> <p>This page is for robots only</p>';
die();
}
$json = file_get_contents('php://input');
$payload = json_decode($json, true);
if (!is_array($payload) || !count($payload)) {
echo '<h1>IoT API</h1> <p>JSON playload required</p>';
die();
}
$db_config = [
'dsn' => 'mysql:host=localhost;dbname=home_iot',
'username' => '...FIXME...',
'password' => '...FIXME...',
'options' => [
\PDO::ATTR_PERSISTENT => true,
],
];
// For PHP 8:
// $dbh = new \PDO(...$db_config);
// For PHP 7.4:
$dbh = new PDO(
$db_config['dsn'],
$db_config['username'],
$db_config['password'],
$db_config['options'],
);
$sth = $dbh->prepare('INSERT INTO `weather`'
. ' (`station`, `temperature`, `pressure`, `humidity`) '
. ' VALUES (:station, :temperature, :pressure, :humidity)');
$keys = ['station', 'temperature', 'pressure', 'humidity'];
foreach ($keys as $key) {
if (array_key_exists($key, $payload)) {
$sth->bindValue($key, $payload[$key]);
} else {
$sth->bindValue($key, null, PDO::PARAM_NULL);
}
}
$sth->execute();
if ($sth->rowCount()) {
echo '<h1>IoT API</h1> <p>OK</p>';
} else {
echo '<h1>IoT API</h1> <p>Error</p>';
}
От тепер все. Поки писав цей пост, в базу потрапило десь приблизно 40 нових вимірювань.