- PVSM.RU - https://www.pvsm.ru -
Задача разработки — быстрая проверка прибора учёта электроэнергии в полевых условиях. Устройство должно обладать низкой стоимостью, высокой мобильностью и более простым интерфейсом в сравнении с аналогом — Энергомонитор-3.3 Т1.
В целом, ничего принципиально нового, это очередной велосипед из ESP и PZEM. В статье я собрал разные, как мне показалось, неочевидные для новичков моменты. Заранее отмечу, что не являюсь профессиональным программистом микроконтроллеров или фронтендером. Я простой инженер, поэтому в статье будет очень много ссылок, которые мне очень помогли.
Писать код для ESP можно хоть в блокноте, лично мне удобнее пользоваться Visual Studio Code [1]. Для этой среды достаточно расширений [2]для программирования на ESP (к примеру - PlatformIO [3]). Espressif IDE [4] для Visual Studio Code существует также в виде самостоятельной IDE [5] (на самом деле она на базе EclipseCDT [6]). (Подробнее - Как и на чём программировать ESP32 и ESP8266 [7], Переползаем с Arduino IDE на VSCode + PlatformIO [8], ESP32 в окружении VSCode [9]).
Мы остановимся на Arduino IDE [10], т. к. она лучше всего подходит для новичков.
ESP8266 NodeMCU V3 [11] - плата на базе wi-fi модуля ESP8266 и USB-UART на CH340, как основа проекта.
3 х PZEM-004T V3.0 [12] - Модуль для замера напряжения, тока, частоты, мощности и суммарно потребленной электроэнергии в кВт/ч.
KY-018 [13]- фоторезистор для считывания импульсов с прибора учёта электроэнергии.
Для работы системы необходимо подключить по инструкции [14] библиотеки:
3.1. https://github.com/mandulaj/PZEM-004T-v30 [15] (в Arduino IDE: инструменты->управление библиотеками - "PZEM-004T-v30").
3.2. https://github.com/esp8266/Arduino [16] (в Arduino IDE: Инструменты->Плата->Менеджер плат - "esp8266 by ESP8266").
3.3. https://github.com/bblanchon/ArduinoJson [17] (в Arduino IDE: Инструменты->Управление библиотеками - "ArduinoJson") или https://arduinojson.org/?utm_source=meta&utm_medium=library.properties [18].
Необходимо также подключить по инструкции [19] дополнительные ссылки для Менеджера плат для NodeMCU: http://arduino.esp8266.com/stable/package_esp8266com_index.json [20] (в Arduino IDE: Файл->Параметры->дополнительные ссылки для Менеджера – вставить ссылку в поле ввода).
Для начала необходимо выбрать режим, в котором будет работать ESP: точка доступа WiFi.mode(WIFI_AP) когда вы подключаетесь к ESP, WiFi.mode(WIFI_STA), когда ESP подключается к вашей точке доступа (роутеру) или совместная работа этих режимов - WiFi.mode(WIFI_AP_STA). Подробнее - ESP32 Useful Wi-Fi Library Functions (Arduino IDE) [21].
Я для наглядности реализовал такую логику: пробуем подключиться к Wi-fi, если не удаётся, то создаём свою точку доступа.
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266WebServer.h>
#include <WiFiUdp.h>
#include <WiFiClient.h>
#define APSSID "SmartGridComMeterESPap" // Имя точки доступа, которую создаст ESP
#define STASSID "Admin" // Точка доступа (логин и пароль от wifi), к которой подключится ESP
#define STAPSK "Admin"
#define STASSID2 "admin"
#define STAPSK2 "admin"
const char *ap_ssid = APSSID;
const char* ssid = STASSID;
const char* password = STAPSK;
const char* ssid2 = STASSID2;
const char* password2 = STAPSK2;
ESP8266WiFiMulti wifiMulti;
ESP8266WebServer server(80);
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_OFF); // Предотвращает проблемы с повторным подключением (слишком долгое подключение)
delay(500);
/*раздел подключения к Wi-Fi*/
WiFi.mode(WIFI_STA);
wifiMulti.addAP(ssid, password);
wifiMulti.addAP(ssid2, password2);
Serial.println("");
Serial.print("Connecting");
// Ожидаем подключения в течении 5 секунд
unsigned long connectionTimer = millis() + 5000;
while (millis() < connectionTimer && wifiMulti.run() != WL_CONNECTED) {
if (wifiMulti.run() != WL_CONNECTED) {
break;
}
delay(500);
Serial.print(".");
}
// Если подключение успешно, отображаем IP-адрес в последовательном мониторе
if (wifiMulti.run() == WL_CONNECTED) {
Serial.println("");
Serial.print("Connected to Network/SSID: ");
Serial.println(WiFi.SSID());
Serial.print("IP address: "); //http://192.168.31.146/
Serial.println(WiFi.localIP()); // IP-адрес, назначенный ESP
} else { //если подключения нет, создаём свою точку доступа
// раздел добавления точки доступа wifi
WiFi.mode(WIFI_AP);
Serial.println("Configuring access point...");
WiFi.softAP(ap_ssid); //Запуск AccessPoint с указанными учетными данными
Serial.print("Access Point Name: ");
Serial.println(ap_ssid);
IPAddress myIP = WiFi.softAPIP(); //IP-адрес нашей точки доступа Esp8266 (где мы можем размещать веб-страницы и просматривать данные)
Serial.print("Access Point IP address: ");
Serial.println(myIP); // http://192.168.4.1/
Serial.println("");
}
}
Вообще, правильным решением будет подключить нормальный WiFiManager [22], который позволит пользователю настраивать сеть самому, к тому же обеспечит более стабильное подключение.
Добавляем в void setup() следующие строчки:
server.on("/", handleRoot);
server.onNotFound(handle_NotFound);
server.begin();
Serial.println("HTTP server started");
Это необходимый минимум в setup() для отображения нашей HTML страницы в браузере.
handleRoot() и handle_NotFound() необходимо реализовать в виде функций, например так:
void handleRoot() {
String html_index_h = webpage; //для обновления HTML/css/js в строку "webpage" в "index.h" запустите "front/htmlToH.exe"
server.send(200, "text/html", html_index_h);
}
void handle_NotFound() {
server.send(404, "text/plain", "Not found");
}
void loop() {
/*// если необходимо контролировать wifi подключение, например по светодиоду
while (wifiMulti.run() != WL_CONNECTED) {
Serial.print(".");
}*/
server.handleClient();
}
Если с handle_NotFound() всё более менее понятно (отправляем на фронт 404-ю ошибку [23] в случае проблем с сервером. т.е. с ESP), то что такое «webpage»? Это наша HTML-страничка в формате строки:
const char webpage[] PROGMEM = R"=====(
<!DOCTYPE html>
<html>
…
</html>)=====";
Внутри круглых скобок хранится код на языке HTML разметки. Обычно скрипты и стили также должны быть встроены в HTML:
const char webpage[] PROGMEM = R"=====(
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
…
</style>
</head>
<script>
…
</script>
…
</html>)=====";
Разработку фронта для проекта лучше выстроить так:
[1] пишем отдельно файлы index.html [24], script.js [25] и style.css [26]
[2] стили и скрипты подключаем к html классически:
<link rel="stylesheet" href="style.css">
<script src="script.js"></script>
[3] Перед тем как заливать прошивку в ESP запускаем скрипт [27], который преобразует наши index.html [24], script.js [25] и style.css [26] в один файл index.h [28]:
#include <string>
#include <iostream>
#include <fstream>
#include <filesystem>
#include <stdlib.h>
std::string WebToStr(std::ifstream& index_html_in) {
std::string result;
std::string line;
if (index_html_in.is_open()) {
while (std::getline(index_html_in, line)) {
char last_line_element = line[line.size() - 1];
if (last_line_element == ';' || last_line_element == '>' || last_line_element == '{' || last_line_element == '}') {
result +="n";
}
if (line.find("<link rel="stylesheet"") != -1) {
line.clear();
line = "<style type="text/css">";
std::ifstream index_html_in("style.css");
line += WebToStr(index_html_in);
line += "n</style>";
}
if (line.find("<script src=") != -1) {
line.clear();
line = "<script>";
std::ifstream index_html_in("script.js");
line += WebToStr(index_html_in);
line += "n</script>";
}
result += line;
}
}
return result;
}
std::string MakeStrFromWeb() {
std::ifstream index_html_in("index.html"); // открываем файл для чтения
std::string html = "const char webpage[] PROGMEM = R"=====(";
html += WebToStr(index_html_in);
html += ")=====";";
const std::filesystem::path CurrentPath = std::filesystem::current_path().parent_path();
std::ofstream index_h_out(CurrentPath/ "srs/PZEM_nodemcu_three_phase/index.h"); // открываем файл для записи
if (index_h_out.is_open()) {
index_h_out << html;
}
index_h_out.close();
return html;
}
int main() {
std::string html = MakeStrFromWeb();
return 0;
}
Не забываем подключить наш index.h [28] к основному проекту:
#include "index.h" //тут хранится наш webpage
void handleRoot() {
String html_index_h = webpage; //для обновления HTML/css/js в строку "webpage" в "index.h" запустите "front/htmlToH.exe"
server.send(200, "text/html", html_index_h);
}
Всю эту процедуру можно автоматизировать, т.е. выполнять обновление index.h [28] при каждой заливке прошивки или при любом изменении index.html [28], script.js [25] и style.css [26] (я в своём проекте пока этим заморачивался). MakeStrFromWeb() можно написать на javascript [29], что будет более логичным в контексте фронт разработки.
Можно также загрузить index.html [28], script.js [25] и style.css [26] в ESP с помощью файловой системы SPIFFS [30]. Подробнее - ESP8266 Web Server using SPIFFS (SPI Flash File System) – NodeMCU [31]
Но расширения, к сожалению не поддерживаются [32]Arduino IDE 2.0.x, только версиями 1.x. Но если хотите, можете скачать [33]старую IDE и заморочиться с SPIFFS (я пробовал – работает). К тому же ARDUINO 1.8.18 работает весьма неплохо.
Тут хитростей нет – если вы правильно подключили сигналы RX/TX от PZEM к ESP, подали напряжения (обязательно, т.к. по цепям напряжения PZEM и дополнительное питание, а без него напряжение будет NaN вместо 0 В) и подключили библиотеку PZEM004Tv30 [34] то значения в мониторе порта (см. пример [35]) вы получите без проблем:
#include <PZEM004Tv30.h>
PZEM004Tv30 pzem1(D1, D2); // (RX,TX) подключиться к TX,RX PZEM1
PZEM004Tv30 pzem2(D5, D6); // (RX,TX) подключиться к TX,RX PZEM2
PZEM004Tv30 pzem3(D7, D0); // (RX,TX) подключиться к TX,RX PZEM3
float current = 0; // суммарный ток
float power = 0; // суммарная мощность
float energy = 0; // суммарная энергия
float voltage1 = 0;
float current1= 0;
float power1= 0;
float energy1= 0;
float frequency1= 0;
float pf1= 0;
float voltage2 = 0;
float current2= 0;
float power2= 0;
float energy2= 0;
float frequency2= 0;
float pf2= 0;
float voltage3 =0;
float current3= 0;
float power3= 0;
float energy3= 0;
float frequency3= 0;
float pf3= 0;
void SetPzem1Values() {
voltage1 = 0;
current1= 0;
power1= 0;
energy1= 0;
frequency1= 0;
pf1= 0;
if (!isnan(voltage1 = pzem1.voltage())) {
current1 = pzem1.current() * currentTransformerTransformationRatio;
current += current1;
frequency1 = pzem1.frequency();
pf1 = pzem1.pf();
power1 = pzem1.power() / WtTokWtScale * currentTransformerTransformationRatio;
power += power1;
energy1 = pzem1.energy() * currentTransformerTransformationRatio;
energy += energy1;
}
}
void SetPzem2Values() {
voltage2 = 0;
current2= 0;
power2= 0;
energy2= 0;
frequency2= 0;
pf2= 0;
if (!isnan(voltage2 = pzem2.voltage())) {
current2 = pzem2.current() * currentTransformerTransformationRatio;
current += current2;
frequency2 = pzem2.frequency();
pf2 = pzem2.pf();
power2 = pzem2.power() / WtTokWtScale * currentTransformerTransformationRatio;
power += power2;
energy2 = pzem2.energy() * currentTransformerTransformationRatio;
energy += energy2;
}
}
void SetPzem3Values() {
voltage3 =0;
current3= 0;
power3= 0;
energy3= 0;
frequency3= 0;
pf3= 0;
if (!isnan(voltage3 = pzem3.voltage())) {
current3 = pzem3.current() * currentTransformerTransformationRatio;
current += current3;
frequency3 = pzem3.frequency();
pf3 = pzem3.pf();
power3 = pzem3.power() / WtTokWtScale * currentTransformerTransformationRatio;
power += power3;
energy3 = pzem3.energy() * currentTransformerTransformationRatio;
energy += energy3;
}
}
void resetCurrentValues() {
yield();
current = 0;
power = 0;
energy = 0;
queueSum = 0;
while (!meterBlinkPeriods.empty()) meterBlinkPeriods.pop();
queueSize = 1;
KYimpNumSumm = 0;
winHi = 0, winLo = 1024;
initWindow();
meterWattage = 0;
constMeterImpsNum = 1000;
yield();
}
Если у вас есть участки программы, которые долго выполняются, то нужно разместить вызовы yield() [36] до и после тяжёлых блоков кода. Также в чужих скетчах можно встретить delay(0), по сути, это и есть yield().
У нас задача отправить это в веб-браузер без обновлений html страницы, как это сделано например в этом [37]проекте с помощью тега <meta http-equiv=refresh content=30>.
Тут нам поможет AJAX [38]– GET и POST запросы. Подробнее - Веб-сервер AJAX на ESP8266: динамическое обновление веб-страниц без их перезагрузки [39].
Проблема проекта выше только в том, что там разработчик отправляет только пару сигналов управления светодиодами в виде текcта, а у нас очень много данных. Отправлять их в формате строки и парсить на стороне браузера клиента весьма накладно. Поэтому воспользуемся JSON [40].
Всё по порядку:
- п.1. Добавляем функцию отправки на наш сервер:
В setup() после строчки server.on("/", handleRoot) пишем: server.on("/pzem_values", SendPzemsValues).
- п.2. Теперь реализуем функцию SendPzemsValues():
#include <ArduinoJson.h>
void SendPzemsValues() {
yield();
current = 0;
power = 0;
energy = 0;
SetPzem1Values();
SetPzem2Values();
SetPzem3Values();
// отправляем ответ в формате json
JsonDocument doc; // создаём JSON документ
// Добавить массивы в JSON документ
JsonArray data = doc["voltages"].to<JsonArray>();
data.add(voltage1);
data.add(voltage2);
data.add(voltage3);
data = doc["currents"].to<JsonArray>();
data.add(current1);
data.add(current2);
data.add(current3);
data = doc["powers"].to<JsonArray>();
data.add(power1);
data.add(power2);
data.add(power3);
data = doc["energies"].to<JsonArray>();
data.add(energy1);
data.add(energy2);
data.add(energy3);
data = doc["frequencies"].to<JsonArray>();
data.add(frequency1);
data.add(frequency2);
data.add(frequency3);
data = doc["powerFactories"].to<JsonArray>();
data.add(pf1);
data.add(pf2);
data.add(pf3);
// Добавить объекты в JSON документ
JsonObject FullValues = doc["FullValues"].to<JsonObject>();
FullValues["current"] = current;
FullValues["power"] = power;
FullValues["energy"] = energy;
server.send(200, "application/json", doc.as<String>());
yield();
}
- п.3. Структура json:
{
"voltages": [
"voltage1",
"voltage2",
"voltage3"
],
"currents": [
"current1",
"current2",
"current3"
],
"powers": [
"power1",
"power2",
"power3"
],
"energies": [
"energy1",
"energy2",
"energy3"
],
"frequencies": [
"frequency1",
"frequency2",
"frequency3"
],
"powerFactories": [
"pf1",
"pf2",
"pf3"
],
"FullValues": {
"current": current,
"power": power,
"energy": energy
}
}
Делаем всё согласно инструкции [38]:
function getPZEMsData() {
var xhttp = new XMLHttpRequest();
xhttp.open("GET", "pzem_values", true);
xhttp.responseType = "json";
xhttp.send();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
console.log("getPZEMsData successful✔️nr");
}
};
xhttp.onload = function () {
ViewAllESPdata(xhttp.response);
};
};
функция ViewAllESPdata(ESPdata) получает JSON, парсит его и выводит в веб-интерфейс с заданным кол-вом точек после запятой. Также она запускает функцию записи всех данных в массив для последующего сохранения в csv [41](подробности см. в репозитории проекта [25])
Обратите внимание на строчку 3:
второй аргумент в методе open -"pzem_values" должен совпадать с первым аргументом метода server.on("/pzem_values", SendPzemsValues) в void setup(), где мы назначаем функцию отправки данных на фронт с ESP.
На фронте для периодического опроса ESP достаточно повесить на кнопку «старт»:
let PZEMinterval;
let ESPsurveyPeriod = 1000; // период опроса ESP в мс
let StartMeterCheckBtn = document.getElementById('StartMeterCheck');
StartMeterCheckBtn.addEventListener('click', startMeterCheck);
function startMeterCheck(e) {
…
PZEMinterval = setInterval(getPZEMsData, ESPsurveyPeriod);
…
}
Остановить опрос (к примеру, по кнопке «стоп») очень просто: clearInterval(PZEMinterval).
Т.к. иногда требуется проверять счётчики трансформаторного включения, необходимо иметь возможность отправить на ESP коэффициент трансформации трансформатора тока. Можно, конечно, хранить Ктт только на фронте в веб-браузере пользователя, но тогда придётся дополнительно обрабатывать получаемые с ESP значения. К тому же, мы не сможем записывать и хранить измерения PZEM с учётом Ктт в постоянной памяти ESP без подключения клиента.
Воспользуемся методом POST:
function sendCurrentTransformerTransformationRatio() {
if (CheckCurrentTransformerTransformationRatioInputs()) {
var xhttp = new XMLHttpRequest();
xhttp.open("POST",
"current_transformer_transformation_ratio?currentTransformerTransformationRatio="+currentTransformerTransformationRatioCheck.value,
true);
xhttp.setRequestHeader("Content-Type", "text; charset=UTF-8");
xhttp.send();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
console.log("sendCurrentTransformerTransformationRatio successful✔️nr");
console.log(this.responseText);
}
};
}
};
Обратите внимание, что теперь отправляем «text» вместо «json».
Второй аргумент в методе open состоит из 3х частей:
«current_transformer_transformation_ratio» - должен совпадать с первым аргуметом в строчке
server.on("/current_transformer_transformation_ratio", SetCurrentTransformerTransformationRatio)
которую мы добавим в void setup() в ESP так же как сделали получение данных PZEM с ESP
«currentTransformerTransformationRatio» - название переменной, которой мы будем пользоваться на ESP для того, чтобы получить Ктт.
Далее прибавляем к этой стоке переменную, которая хранит Ктт и которую пользователь задал на интерфейсе.
// Настройка HTTP-сервера
server.on("/", handleRoot);
server.on("/current_transformer_transformation_ratio", SetCurrentTransformerTransformationRatio);
server.on("/pzem_values", SendPzemsValues);
server.on("/reset", Reset);
server.onNotFound(handle_NotFound);
server.begin();
Serial.println("HTTP server started");
Теперь необходимо написать функцию SetCurrentTransformerTransformationRatio(), которая будет устанавливать Ктт на ESP:
void SetCurrentTransformerTransformationRatio() {
String CurrentTransformerTransformationRatioStr = server.arg("currentTransformerTransformationRatio");
currentTransformerTransformationRatio = CurrentTransformerTransformationRatioStr.toInt();
server.send(200, "text/plane", "currentTransformerTransformationRatio has been set");
}
Тут мы как раз используем вторую и третью часть второго аргумента метода open, который мы задавали на фронте в script.js [25], для того, чтобы вытащить Ктт.
Примерно также мы будем сбрасывать PZEMы с помощью функции Reset() в script.js [25]:
function Reset() {
var xhttp = new XMLHttpRequest();
xhttp.open("GET", "reset", true);
xhttp.responseType = "text";
xhttp.send();
xhttp.onload = function () {
console.log(this.responseText);
};
};
Только в данном случае мы не отправляем никаких данных на ESP:
void Reset() {
resetCurrentValues();
currentTransformerTransformationRatio = 1;
if (pzem1.resetEnergy() &&
pzem2.resetEnergy() &&
pzem3.resetEnergy()) {
server.send(200, "text/plane", "Energy in pzems has been reset");
} else {
server.send(200, "text/plane", "power reset error!");
}
}
Пожалуй, это самая сложная и противоречивая часть проекта.
Алгоритм считывания импульсов моргания светодиода прибора учёта давно придуман за нас и подробно описан - Подключаем ардуино к счётчику [42].
С незначительными изменениями он был перенесён в проект (см meterBlinkPeriodCalc.h [43]). Добавлена возможность рассчитывать погрешность на основе нескольких подряд возникающих импульсов, как это реализовано в Энергомонитор-3.3 Т1 [44]. Длину очереди импульсов size_t queueSize (основная логика крутится вокруг FIFO [45] std::queue<double> meterBlinkPeriods) можно задавать из веб-интерфейса.
void loop() {
server.handleClient();
delay(10);
checkLedState();
}
Всё это прекрасно работает до тех пор, пока мы не начинаем опрашивать ESP. Начинаются пропуски импульсов и расчёт погрешности становится некорректным.
Всё потому, что на ESP возникает несколько задач, которые он не может решать параллельно. На этом этапе нам следует воспользоваться ОСРВ [46] например ESP8266_RTOS_SDK [47].
Но мы попробуем обойтись малой кровью. Необходимо выполнять checkLedState() за пределами void loop(), что мы и сделаем вместо того, чтобы настраивать millis().
void SendPzemsValues() {
yield(); checkLedState();// костыльно решаем проблему многозадачности
current = 0; checkLedState();
power = 0;
energy = 0; checkLedState();
SetPzem1Values(); checkLedState();
SetPzem2Values(); checkLedState();
SetPzem3Values(); checkLedState();
// отправляем ответ в формате json
JsonDocument doc; checkLedState(); // создаём JSON документ
// Добавить массивы в JSON документ
JsonArray data = doc["voltages"].to<JsonArray>(); checkLedState();
data.add(voltage1); checkLedState();
data.add(voltage2); checkLedState();
data.add(voltage3); checkLedState();
data = doc["currents"].to<JsonArray>(); checkLedState();
data.add(current1); checkLedState();
data.add(current2); checkLedState();
data.add(current3); checkLedState();
data = doc["powers"].to<JsonArray>(); checkLedState();
data.add(power1); checkLedState();
data.add(power2); checkLedState();
data.add(power3); checkLedState();
data = doc["energies"].to<JsonArray>(); checkLedState();
data.add(energy1); checkLedState();
data.add(energy2); checkLedState();
data.add(energy3); checkLedState();
data = doc["frequencies"].to<JsonArray>(); checkLedState();
data.add(frequency1); checkLedState();
data.add(frequency2); checkLedState();
data.add(frequency3); checkLedState();
data = doc["powerFactories"].to<JsonArray>(); checkLedState();
data.add(pf1); checkLedState();
data.add(pf2); checkLedState();
data.add(pf3); checkLedState();
// Добавить объекты в JSON документ
JsonObject FullValues = doc["FullValues"].to<JsonObject>(); checkLedState();
FullValues["current"] = current; checkLedState();
FullValues["power"] = power; checkLedState();
FullValues["energy"] = energy; checkLedState();
JsonObject ResSMDValues = doc["ResSMDValues"].to<JsonObject>(); checkLedState();
ResSMDValues["KYimpNumSumm"] = KYimpNumSumm; checkLedState();
ResSMDValues["SMDimpPeriod"] = meterBlinkPeriod; checkLedState();
if (printSMDAccuracy) {
ResSMDValues["SMDpower"] = meterWattage; checkLedState();
if (power) ResSMDValues["SMDAccuracy"] = (power - meterWattage) / power * 100; checkLedState();
printSMDAccuracy = false;
}
server.send(200, "application/json", doc.as<String>()); checkLedState();
yield(); checkLedState();
}
В комментариях на меня накинутся за такой код и будут вполне правы. Это топорное, но простое и быстрое «решение». Для серьёзной разработки оно, конечно, не годится.
Но. Во-первых, это работает. Проверено при периоде GET-запросов 1 с и при различных частотах моргания разных приборов учёта.
Во-вторых, нет необходимости создавать отдельную задачу опроса ESP каждый раз при получении GET request. Ну или нам пришлось бы создавать «жёсткую» статическую задачу отправки PZEM-данных с ESP при страте ESP. Т.е. отправлять данные без запроса со стороны веб-браузера, а поэтому пришлось бы разбираться с веб-сокетами [49].
Мощность прибора учёта электроэнергии рассчитывается по следующей формуле:
где n - кол-во импульсов;
А - передаточное число ПУ (постоянная счётчика пишется на корпусе рядом с моргающим светодиодом), имп/кВ*ч;
t - время, с.
время между импульсами вычисляется внутри checkLedState():
...
unsigned long microTimer; // Стоп-таймер в микросекундах
double meterBlinkPeriod; // Период моргания счётчика
boolean ledState, ledStateOld; // текущее логическое состояние фоторезистора
...
void checkLedState() {
...
ledStateOld = ledState; // сохраняем в буфер старое значение уровня сенсора
...
if (ledStateOld && !ledState) { // ИНДикатор только что загорелся
...
meterBlinkPeriod = double(micros() - microTimer) / 1000000;// длина последнего импульса = текущее время - время прошлого перехода
microTimer = micros(); // запоминаем время этого перехода в таймер
...
}
...
}
Если пользователь на фронте в веб-интерфейсе задал число импульсов, необходимое для расчёта погрешности, то все длины последовательных импульсов записываются в очередь такого же размера:
...
#include <queue>
...
std::queue<double> meterBlinkPeriods; // Очередь из последних периодов моргания счётчика
size_t queueSize = 1;
double queueSum = 0;
...
void checkLedState() {
...
if (queueSize > 1) {
printSMDAccuracy = false; //запрещаем отправлять погрешность на фронт
meterBlinkPeriods.push(meterBlinkPeriod); // добавляем период моргания в очередь, если пользователь задал её длину > 1
queueSum += meterBlinkPeriod;
if (meterBlinkPeriods.size() == queueSize) { //если очередь переполнена то
/*queueSum -= meterAccuracy.front(); // корректируем сумму очереди
meterAccuracy.pop(); // удаляем первый элемент, если очередь переполнена*/
meterBlinkPeriod = queueSum / meterBlinkPeriods.size(); // рассчитываем среднюю длину импульса
while (!meterBlinkPeriods.empty()) meterBlinkPeriods.pop(); // очищаем очередь
queueSum = 0; // обнуляем сумму длин импульсов
meterWattage = 3600 / meterBlinkPeriod / constMeterImpsNum; // нагрузка (кВт) = кол-во таких импульсов в часе разделить на имп за 1кВт*ч
printSMDAccuracy = true; //разрешаем отправлять погрешность на фронт
}
}
...
}
Погрешность в % вычисляется по формуле:
Погрешность лучше вычислять непосредственно перед отправкой на фронт в формате json:
if (printSMDAccuracy) {
ResSMDValues["SMDpower"] = meterWattage;checkLedState();
if (power) ResSMDValues["SMDAccuracy"] = (power - meterWattage) / power * 100;checkLedState();
printSMDAccuracy = false;
}
Корпусом, проводами и комплектующими занимался мой коллега, поэтому детально процесс описывать не буду.
Корпус был взят готовый [50](ищите – «Корпус для РЭА пластиковый настольный RUICHI»). Разъёмы были скопированы у РЕТОМЕТР-М3 [51] Трансформаторы тока и фоторезистор, которые входили в состав PZEM переделаны: были припаяны jack 3.5 штекеры для быстрого подключения к корпусу (в который были встроены AUX порты). Для цепей напряжения используется кабель общего назначения КОН 61.04 [52]. Шасси и корпус для датчика были напечатаны на 3D-принтере.
Репозиторий проекта [53]
Особые благодарности источникам:
1. Подключаем Ардуино К Счётчику [42]
2. Подключение нескольких PZEM-004t на ESP [37]
3. Веб-сервер AJAX на ESP8266: динамическое обновление веб-страниц без их перезагрузки [39]
Автор: airattu
Источник [54]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/arduino/410149
Ссылки в тексте:
[1] Visual Studio Code: https://code.visualstudio.com/
[2] расширений : https://randomnerdtutorials.com/vs-code-platformio-ide-esp32-esp8266-arduino/
[3] PlatformIO: https://platformio.org/platformio-ide
[4] Espressif IDE: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/index.html
[5] самостоятельной IDE: https://dl.espressif.com/dl/esp-idf/?idf=5.1
[6] EclipseCDT: https://projects.eclipse.org/projects/tools.cdt
[7] Как и на чём программировать ESP32 и ESP8266: https://kotyara12.ru/iot/esp_start/
[8] Переползаем с Arduino IDE на VSCode + PlatformIO: https://kotyara12.ru/iot/crawl-to-pio/
[9] ESP32 в окружении VSCode: https://habr.com/ru/articles/530638/
[10] Arduino IDE: https://www.arduino.cc/en/software
[11] ESP8266 NodeMCU V3: https://%20https:/arduinomaster.ru/platy-arduino/esp8266-nodemcu-v3-lua/
[12] 3 х PZEM-004T V3.0: https://%20https:/innovatorsguru.com/wp-content/uploads/2019/06/PZEM-004T-V3.0-Datasheet-User-Manual.pdf
[13] KY-018 : https://eclass.uth.gr/modules/document/file.php/E-CE_U_269/Sensors/Sensors_%20Datasheets/KY-018-Joy-IT.pd
[14] по инструкции: https://wiki.iarduino.ru/page/Installing_libraries
[15] https://github.com/mandulaj/PZEM-004T-v30: https://github.com/mandulaj/PZEM-004T-v30
[16] https://github.com/esp8266/Arduino: https://github.com/esp8266/Arduino
[17] https://github.com/bblanchon/ArduinoJson: https://github.com/bblanchon/ArduinoJson
[18] https://arduinojson.org/?utm_source=meta&utm_medium=library.properties: https://arduinojson.org/?utm_source=meta&utm_medium=library.properties
[19] инструкции: https://robotclass.ru/articles/node-mcu-arduino-ide-setup/
[20] http://arduino.esp8266.com/stable/package_esp8266com_index.json: http://arduino.esp8266.com/stable/package_esp8266com_index.json
[21] ESP32 Useful Wi-Fi Library Functions (Arduino IDE): https://randomnerdtutorials.com/esp32-useful-wi-fi-functions-arduino/
[22] подключить нормальный WiFiManager: https://github.com/tzapu/WiFiManager
[23] 404-ю ошибку: https://ru.wikipedia.org/wiki/%D0%9E%D1%88%D0%B8%D0%B1%D0%BA%D0%B0_404
[24] index.html: https://github.com/Ayrat123T/Arduino_PZEM-nodemcu-web-wifi_three_phase_electricity_meter/blob/main/front/index.html
[25] script.js: https://github.com/Ayrat123T/Arduino_PZEM-nodemcu-web-wifi_three_phase_electricity_meter/blob/main/front/script.js
[26] style.css: https://github.com/Ayrat123T/Arduino_PZEM-nodemcu-web-wifi_three_phase_electricity_meter/blob/main/front/style.css
[27] скрипт: https://github.com/Ayrat123T/Arduino_PZEM-nodemcu-web-wifi_three_phase_electricity_meter/blob/main/front/htmlToH.cpp
[28] index.h: https://github.com/Ayrat123T/Arduino_PZEM-nodemcu-web-wifi_three_phase_electricity_meter/blob/main/srs/PZEM_nodemcu_three_phase/index.h
[29] можно написать на javascript: https://github.com/tzapu/WiFiManager/blob/master/extras/parse.js
[30] SPIFFS: https://amperkot.ru/blog/esp-spiffs/?srsltid=AfmBOop6dRzyBR1e3GQaN9ZqxOoA70iNpD8Httbj2FyRlWnbHdzCTs7C
[31] ESP8266 Web Server using SPIFFS (SPI Flash File System) – NodeMCU: https://randomnerdtutorials.com/esp8266-web-server-spiffs-nodemcu/
[32] не поддерживаются : https://forum.arduino.cc/t/using-the-filesystem-spiffs-with-arduino-ide-2-0-x-is-problematic/1162671
[33] скачать : https://www.arduino.cc/en/software/OldSoftwareReleases
[34] библиотеку PZEM004Tv30: https://github.com/mandulaj/PZEM-004T-v30/tree/master
[35] пример: https://github.com/mandulaj/PZEM-004T-v30/blob/master/examples/PZEMHardSerial/PZEMHardSerial.cpp
[36] yield(): https://alexgyver.ru/lessons/esp8266/
[37] этом : https://rain.linuxoid.in/2021/03/07/%D0%BF%D0%BE%D0%B4%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BD%D0%B5%D1%81%D0%BA%D0%BE%D0%BB%D1%8C%D0%BA%D0%B8%D1%85-pzem-004t-%D0%BD%D0%B0-esp/
[38] AJAX : https://developer.mozilla.org/ru/docs/Learn_web_development/Core/Scripting/Network_requests
[39] Веб-сервер AJAX на ESP8266: динамическое обновление веб-страниц без их перезагрузки: https://microkontroller.ru/esp8266-projects/veb-server-ajax-na-esp8266-dinamicheskoe-obnovlenie-veb-stranicz-bez-ih-perezagruzki/
[40] воспользуемся JSON: https://developer.mozilla.org/ru/docs/Learn_web_development/Core/Scripting/JSON
[41] csv : https://github.com/Ayrat123T/Arduino_PZEM-nodemcu-web-wifi_three_phase_electricity_meter/blob/main/2025_1_21%2016_16_27%20exportSmartGridComMeterData.csv
[42] Подключаем ардуино к счётчику: https://www.instructables.com/%D0%9F%D0%BE%D0%B4%D0%BA%D0%BB%D1%8E%D1%87%D0%B0%D0%B5%D0%BC-%D0%B0%D1%80%D0%B4%D1%83%D0%B8%D0%BD%D0%BE-%D0%BA-%D1%81%D1%87%D1%91%D1%82%D1%87%D0%B8%D0%BA%D1%83/
[43] meterBlinkPeriodCalc.h: https://github.com/Ayrat123T/Arduino_PZEM-nodemcu-web-wifi_three_phase_electricity_meter/blob/main/srs/PZEM_nodemcu_three_phase/meterBlinkPeriodCalc.h
[44] Энергомонитор-3.3 Т1: https://www.mars-energo.ru/home/poverochnye-ustanovki-i-etalony-dlya-elektroschetchikov/etalonnyie-schetchiki/energomonitor-3.3t1-s.html
[45] FIFO: https://ru.wikipedia.org/wiki/FIFO
[46] ОСРВ: https://ru.wikipedia.org/wiki/%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D0%B0%D1%8F_%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0_%D1%80%D0%B5%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B3%D0%BE_%D0%B2%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%B8
[47] ESP8266_RTOS_SDK: https://github.com/espressif/ESP8266_RTOS_SDK
[48] Многозадачность в Arduino: https://alexgyver.ru/lessons/how-to-sketch/
[49] веб-сокетами: https://ru.stackoverflow.com/questions/684112/%D0%92%D0%BE%D0%B7%D0%BC%D0%BE%D0%B6%D0%BD%D0%BE-%D0%BB%D0%B8-%D0%BE%D1%82%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D1%82%D1%8C-response-%D0%B1%D0%B5%D0%B7-%D0%B7%D0%B0%D0%BF%D1%80%D0%BE%D1%81%D0%25B
[50] готовый : https://ruichi.ru/ustanovochnye-izdeliya/korpusa-dlya-rea/korpus-dlia-rea-ruichi-15-12-195kh175kh70-nastolnyi/
[51] РЕТОМЕТР-М3: https://dynamics.com.ru/production/retom-21-complex/retometr-m3
[52] КОН 61.04: https://dynamics.com.ru/userfiles/file/product/dka-61/dka.pdf
[53] Репозиторий проекта: https://github.com/Ayrat123T/Arduino_PZEM-nodemcu-web-wifi_three_phase_electricity_meter/tree/main
[54] Источник: https://habr.com/ru/articles/880682/?utm_source=habrahabr&utm_medium=rss&utm_campaign=880682
Нажмите здесь для печати.