Задача разработки — быстрая проверка прибора учёта электроэнергии в полевых условиях. Устройство должно обладать низкой стоимостью, высокой мобильностью и более простым интерфейсом в сравнении с аналогом — Энергомонитор-3.3 Т1.

В целом, ничего принципиально нового, это очередной велосипед из ESP и PZEM. В статье я собрал разные, как мне показалось, неочевидные для новичков моменты. Заранее отмечу, что не являюсь профессиональным программистом микроконтроллеров или фронтендером. Я простой инженер, поэтому в статье будет очень много ссылок, которые мне очень помогли.
1. IDE
Выбор IDE
Писать код для ESP можно хоть в блокноте, лично мне удобнее пользоваться Visual Studio Code. Для этой среды достаточно расширений для программирования на ESP (к примеру - PlatformIO). Espressif IDE для Visual Studio Code существует также в виде самостоятельной IDE (на самом деле она на базе EclipseCDT). (Подробнее - Как и на чём программировать ESP32 и ESP8266, Переползаем с Arduino IDE на VSCode + PlatformIO, ESP32 в окружении VSCode).
Мы остановимся на Arduino IDE, т. к. она лучше всего подходит для новичков.
2. Аппаратная часть
ESP8266 NodeMCU V3 - плата на базе wi-fi модуля ESP8266 и USB-UART на CH340, как основа проекта.
3 х PZEM-004T V3.0 - Модуль для замера напряжения, тока, частоты, мощности и суммарно потребленной электроэнергии в кВт/ч.
KY-018 - фоторезистор для считывания импульсов с прибора учёта электроэнергии.

3. Настройка Arduino IDE
Для работы системы необходимо подключить по инструкции библиотеки:
3.1. https://github.com/mandulaj/PZEM-004T-v30 (в Arduino IDE: инструменты->управление библиотеками - "PZEM-004T-v30").
3.2. https://github.com/esp8266/Arduino (в Arduino IDE: Инструменты->Плата->Менеджер плат - "esp8266 by ESP8266").
3.3. https://github.com/bblanchon/ArduinoJson (в Arduino IDE: Инструменты->Управление библиотеками - "ArduinoJson") или https://arduinojson.org/?utm_source=meta&utm_medium=library.properties.
Необходимо также подключить по инструкции дополнительные ссылки для Менеджера плат для NodeMCU: http://arduino.esp8266.com/stable/package_esp8266com_index.json (в Arduino IDE: Файл->Параметры->дополнительные ссылки для Менеджера – вставить ссылку в поле ввода).
4. Программная часть

4.1 Wi-fi
Для начала необходимо выбрать режим, в котором будет работать ESP: точка доступа WiFi.mode(WIFI_AP)
когда вы подключаетесь к ESP, WiFi.mode(WIFI_STA)
, когда ESP подключается к вашей точке доступа (роутеру) или совместная работа этих режимов - WiFi.mode(WIFI_AP_STA)
. Подробнее - ESP32 Useful Wi-Fi Library Functions (Arduino IDE).
Я для наглядности реализовал такую логику: пробуем подключиться к Wi-fi, если не удаётся, то создаём свою точку доступа.
настройка Wi-fi в .ino
#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, который позволит пользователю настраивать сеть самому, к тому же обеспечит более стабильное подключение.
4.2. Настройка HTTP-сервера
Добавляем в 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-ю ошибку в случае проблем с сервером. т.е. с 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>)=====";
4.3. Пишем фронтенд

Разработку фронта для проекта лучше выстроить так:
[1] пишем отдельно файлы index.html, script.js и style.css
[2] стили и скрипты подключаем к html классически:
<link rel="stylesheet" href="style.css">
<script src="script.js"></script>
[3] Перед тем как заливать прошивку в ESP запускаем скрипт, который преобразует наши index.html, script.js и style.css в один файл index.h:
htmlToH.cpp:
#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 к основному проекту:
#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 при каждой заливке прошивки или при любом изменении index.html, script.js и style.css (я в своём проекте пока этим заморачивался). MakeStrFromWeb()
можно написать на javascript, что будет более логичным в контексте фронт разработки.
Можно также загрузить index.html, script.js и style.css в ESP с помощью файловой системы SPIFFS. Подробнее - ESP8266 Web Server using SPIFFS (SPI Flash File System) – NodeMCU
Но расширения, к сожалению не поддерживаются Arduino IDE 2.0.x, только версиями 1.x. Но если хотите, можете скачать старую IDE и заморочиться с SPIFFS (я пробовал – работает). К тому же ARDUINO 1.8.18 работает весьма неплохо.
4.4 Измерения с помощью PZEM
Тут хитростей нет – если вы правильно подключили сигналы RX/TX от PZEM к ESP, подали напряжения (обязательно, т.к. по цепям напряжения PZEM и дополнительное питание, а без него напряжение будет NaN
вместо 0 В) и подключили библиотеку PZEM004Tv30 то значения в мониторе порта (см. пример) вы получите без проблем:
setValues.h:
#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() до и после тяжёлых блоков кода. Также в чужих скетчах можно встретить delay(0)
, по сути, это и есть yield()
.
У нас задача отправить это в веб-браузер без обновлений html страницы, как это сделано например в этом проекте с помощью тега <meta http-equiv=refresh content=30>
.
4.5 Отправка данных PZEM c ESP на фронт в формате JSON
Тут нам поможет AJAX – GET и POST запросы. Подробнее - Веб-сервер AJAX на ESP8266: динамическое обновление веб-страниц без их перезагрузки.
Проблема проекта выше только в том, что там разработчик отправляет только пару сигналов управления светодиодами в виде текcта, а у нас очень много данных. Отправлять их в формате строки и парсить на стороне браузера клиента весьма накладно. Поэтому воспользуемся JSON.
Всё по порядку:
- п.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
}
}
4.6 Запрос данных PZEM с ESP на фронт в формате JSON
Делаем всё согласно инструкции:
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 (подробности см. в репозитории проекта)
Обратите внимание на строчку 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)
.
4.7 Отправка данных или команд с фронта на ESP
Т.к. иногда требуется проверять счётчики трансформаторного включения, необходимо иметь возможность отправить на 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 для того, чтобы получить Ктт.
Далее прибавляем к этой стоке переменную, которая хранит Ктт и которую пользователь задал на интерфейсе.
4.8 Получение данных с фронта на 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, для того, чтобы вытащить Ктт.
Примерно также мы будем сбрасывать PZEMы с помощью функции Reset()
в script.js:
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!");
}
}
4.9 Считывание импульсов с прибора учёта с помощью фоторезистора
Пожалуй, это самая сложная и противоречивая часть проекта.
Алгоритм считывания импульсов моргания светодиода прибора учёта давно придуман за нас и подробно описан - Подключаем ардуино к счётчику.
С незначительными изменениями он был перенесён в проект (см meterBlinkPeriodCalc.h). Добавлена возможность рассчитывать погрешность на основе нескольких подряд возникающих импульсов, как это реализовано в Энергомонитор-3.3 Т1. Длину очереди импульсов size_t queueSize
(основная логика крутится вокруг FIFO std::queue<double> meterBlinkPeriods
) можно задавать из веб-интерфейса.
void loop() {
server.handleClient();
delay(10);
checkLedState();
}
Всё это прекрасно работает до тех пор, пока мы не начинаем опрашивать ESP. Начинаются пропуски импульсов и расчёт погрешности становится некорректным.
Всё потому, что на ESP возникает несколько задач, которые он не может решать параллельно. На этом этапе нам следует воспользоваться ОСРВ например ESP8266_RTOS_SDK.

Но мы попробуем обойтись малой кровью. Необходимо выполнять 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. Т.е. отправлять данные без запроса со стороны веб-браузера, а поэтому пришлось бы разбираться с веб-сокетами.
4.10 Расчёт мощности и погрешности прибора учёта электроэнергии по импульсам и измерениям PZEM
Мощность прибора учёта электроэнергии рассчитывается по следующей формуле:
где 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;
}
5. Корпус
Корпусом, проводами и комплектующими занимался мой коллега, поэтому детально процесс описывать не буду.

Корпус был взят готовый (ищите – «Корпус для РЭА пластиковый настольный RUICHI»). Разъёмы были скопированы у РЕТОМЕТР-М3 Трансформаторы тока и фоторезистор, которые входили в состав PZEM переделаны: были припаяны jack 3.5 штекеры для быстрого подключения к корпусу (в который были встроены AUX порты). Для цепей напряжения используется кабель общего назначения КОН 61.04. Шасси и корпус для датчика были напечатаны на 3D-принтере.

Особые благодарности источникам:
1. Подключаем Ардуино К Счётчику
2. Подключение нескольких PZEM-004t на ESP
3. Веб-сервер AJAX на ESP8266: динамическое обновление веб-страниц без их перезагрузки
Автор: airattu