Разработка трёхфазного энергомонитора на базе ESP8266 с функцией автоматической проверки прибора учёта

от автора

Задача разработки — быстрая проверка прибора учёта электроэнергии в полевых условиях. Устройство должно обладать низкой стоимостью, высокой мобильностью и более простым интерфейсом в сравнении с аналогом — Энергомонитор-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✔️\n\r");       }     };     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✔️\n\r");                 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.

Многозадачность в Arduino

Но мы попробуем обойтись малой кровью. Необходимо выполнять 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

Мощность прибора учёта электроэнергии рассчитывается по следующей формуле:

P=(3600*n)/(A*t)

где 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; //разрешаем отправлять погрешность на фронт         }     }   ... } 

Погрешность в % вычисляется по формуле:

Р=(Рpzem-Рпу)/Рpzem*100%

Погрешность лучше вычислять непосредственно перед отправкой на фронт в формате 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: динамическое обновление веб-страниц без их перезагрузки


ссылка на оригинал статьи https://habr.com/ru/articles/880682/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *