Задача разработки — быстрая проверка прибора учёта электроэнергии в полевых условиях. Устройство должно обладать низкой стоимостью, высокой мобильностью и более простым интерфейсом в сравнении с аналогом — Энергомонитор-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.
Но мы попробуем обойтись малой кровью. Необходимо выполнять 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: динамическое обновление веб-страниц без их перезагрузки
ссылка на оригинал статьи https://habr.com/ru/articles/880682/
Добавить комментарий