From 15de2955fbe42cf30106b643081000a518005369 Mon Sep 17 00:00:00 2001 From: Mit4el Date: Mon, 12 Feb 2024 20:49:36 +0300 Subject: [PATCH 01/22] smartBoiler --- src/modules/exec/OpenThermSlave/OpenTherm.cpp | 812 ++++++++++++++++++ src/modules/exec/OpenThermSlave/OpenTherm.h | 208 +++++ .../exec/OpenThermSlave/OpenThermSlave.cpp | 589 +++++++++++++ src/modules/exec/OpenThermSlave/modinfo.json | 111 +++ src/modules/exec/SmartBoiler/BoilerHeader.h | 78 ++ src/modules/exec/SmartBoiler/SmartBoiler.cpp | 582 +++++++++++++ src/modules/exec/SmartBoiler/modinfo.json | 136 +++ src/modules/exec/SmartBoiler/readme.txt | 71 ++ src/modules/exec/SmartBoiler/smartBoiler.json | 337 ++++++++ 9 files changed, 2924 insertions(+) create mode 100644 src/modules/exec/OpenThermSlave/OpenTherm.cpp create mode 100644 src/modules/exec/OpenThermSlave/OpenTherm.h create mode 100644 src/modules/exec/OpenThermSlave/OpenThermSlave.cpp create mode 100644 src/modules/exec/OpenThermSlave/modinfo.json create mode 100644 src/modules/exec/SmartBoiler/BoilerHeader.h create mode 100644 src/modules/exec/SmartBoiler/SmartBoiler.cpp create mode 100644 src/modules/exec/SmartBoiler/modinfo.json create mode 100644 src/modules/exec/SmartBoiler/readme.txt create mode 100644 src/modules/exec/SmartBoiler/smartBoiler.json diff --git a/src/modules/exec/OpenThermSlave/OpenTherm.cpp b/src/modules/exec/OpenThermSlave/OpenTherm.cpp new file mode 100644 index 00000000..8f6f7467 --- /dev/null +++ b/src/modules/exec/OpenThermSlave/OpenTherm.cpp @@ -0,0 +1,812 @@ +/* +OpenTherm.cpp - OpenTherm Communication Library For Arduino, ESP8266 +Copyright 2018, Ihor Melnyk +*/ + +#include "OpenTherm.h" + +OpenTherm::OpenTherm(int inPin, int outPin, bool isSlave) : status(OpenThermStatus::NOT_INITIALIZED), + inPin(inPin), + outPin(outPin), + isSlave(isSlave), + response(0), + responseStatus(OpenThermResponseStatus::NONE), + responseTimestamp(0), + handleInterruptCallback(NULL), + processResponseCallback(NULL) +{ + imitFlag = false; +} + +void OpenTherm::begin(void (*handleInterruptCallback)(void), void (*processResponseCallback)(unsigned long, OpenThermResponseStatus)) +{ + pinMode(inPin, INPUT); + pinMode(outPin, OUTPUT); + if (handleInterruptCallback != NULL) + { + this->handleInterruptCallback = handleInterruptCallback; + attachInterrupt(digitalPinToInterrupt(inPin), handleInterruptCallback, CHANGE); + } + activateBoiler(); + status = OpenThermStatus::READY; + this->processResponseCallback = processResponseCallback; +} + +void OpenTherm::begin(void (*handleInterruptCallback)(void)) +{ + begin(handleInterruptCallback, NULL); +} + +bool IRAM_ATTR OpenTherm::isReady() +{ + return status == OpenThermStatus::READY; +} + +int IRAM_ATTR OpenTherm::readState() +{ + return digitalRead(inPin); +} + +void OpenTherm::setActiveState() +{ + digitalWrite(outPin, LOW); +} + +void OpenTherm::setIdleState() +{ + digitalWrite(outPin, HIGH); +} + +void OpenTherm::activateBoiler() +{ + setIdleState(); + delay(1000); +} + +void OpenTherm::sendBit(bool high) +{ + if (high) + setActiveState(); + else + setIdleState(); + delayMicroseconds(500); + if (high) + setIdleState(); + else + setActiveState(); + delayMicroseconds(500); +} + +bool OpenTherm::sendRequestAync(unsigned long request) +{ + // Serial.println("Request: " + String(request, HEX)); + noInterrupts(); + const bool ready = isReady(); + interrupts(); + + if (!ready) + return false; + + status = OpenThermStatus::REQUEST_SENDING; + response = 0; + responseStatus = OpenThermResponseStatus::NONE; +// Prevent switching to other tasks as there is a delay within sendBit +#ifdef ESP32 +// vTaskSuspendAll(); +#endif + sendBit(HIGH); // start bit + for (int i = 31; i >= 0; i--) + { + sendBit(bitRead(request, i)); + } + sendBit(HIGH); // stop bit + setIdleState(); +#ifdef ESP32 +// xTaskResumeAll(); +#endif + status = OpenThermStatus::RESPONSE_WAITING; + responseTimestamp = micros(); + if (imitFlag) + ImitationResponse(request); + return true; +} + +unsigned long OpenTherm::sendRequest(unsigned long request) +{ + if (!sendRequestAync(request)) + return 0; + while (!isReady()) + { + process(); + yield(); + } + return response; +} + +bool OpenTherm::sendResponse(unsigned long request) +{ + status = OpenThermStatus::REQUEST_SENDING; + response = 0; + responseStatus = OpenThermResponseStatus::NONE; + // Prevent switching to other tasks as there is a delay within sendBit +#ifdef ESP32 +// vTaskSuspendAll(); +#endif + sendBit(HIGH); // start bit + for (int i = 31; i >= 0; i--) + { + sendBit(bitRead(request, i)); + } + sendBit(HIGH); // stop bit + setIdleState(); +#ifdef ESP32 +// xTaskResumeAll(); +#endif + status = OpenThermStatus::READY; + return true; +} + +unsigned long OpenTherm::getLastResponse() +{ + return response; +} + +OpenThermResponseStatus OpenTherm::getLastResponseStatus() +{ + return responseStatus; +} + +void IRAM_ATTR OpenTherm::handleInterrupt() +{ + if (isReady()) + { + if (isSlave && readState() == HIGH) + { + status = OpenThermStatus::RESPONSE_WAITING; + } + else + { + return; + } + } + + unsigned long newTs = micros(); + if (status == OpenThermStatus::RESPONSE_WAITING) + { + if (readState() == HIGH) + { + status = OpenThermStatus::RESPONSE_START_BIT; + responseTimestamp = newTs; + } + else + { + // Error start bit / Ошибка стартового бита + status = OpenThermStatus::RESPONSE_INVALID; + responseTimestamp = newTs; + } + } + else if (status == OpenThermStatus::RESPONSE_START_BIT) + { + if ((newTs - responseTimestamp < 750) && readState() == LOW) + { + status = OpenThermStatus::RESPONSE_RECEIVING; + responseTimestamp = newTs; + responseBitIndex = 0; + } + else + { + // Error Start_bit LOW 750mks / Ошибка стартового бита по тылу (нет LOW через 750мкс) + status = OpenThermStatus::RESPONSE_INVALID; + responseTimestamp = newTs; + } + } + else if (status == OpenThermStatus::RESPONSE_RECEIVING) + { + // unsigned long bitDuration = newTs - responseTimestamp; + // В новой спецификации стоповый бит не обязателен. Если не дождались, всё равно попробуем разобрать + if ((newTs - responseTimestamp) > 750 && (newTs - responseTimestamp) < 1300) + { + if (responseBitIndex < 32) + { + response = (response << 1) | !readState(); + responseTimestamp = newTs; + responseBitIndex++; + } + else + { // stop bit + status = OpenThermStatus::RESPONSE_READY; + responseTimestamp = newTs; + } + } + } +} + +void OpenTherm::process() +{ + noInterrupts(); + OpenThermStatus st = status; + unsigned long ts = responseTimestamp; + interrupts(); + + if (st == OpenThermStatus::READY) + return; + unsigned long newTs = micros(); + if (st != OpenThermStatus::NOT_INITIALIZED && st != OpenThermStatus::DELAY && (newTs - ts) > 1000000) + { + status = OpenThermStatus::READY; + responseStatus = OpenThermResponseStatus::TIMEOUT; + if (processResponseCallback != NULL) + { + processResponseCallback(response, responseStatus); + } + } + else if (st == OpenThermStatus::RESPONSE_INVALID) + { + status = OpenThermStatus::DELAY; + responseStatus = OpenThermResponseStatus::INVALID; + if (processResponseCallback != NULL) + { + processResponseCallback(response, responseStatus); + } + } + else if (st == OpenThermStatus::RESPONSE_READY) + { + status = OpenThermStatus::DELAY; + responseStatus = (isSlave ? isValidRequest(response) : isValidResponse(response)) ? OpenThermResponseStatus::SUCCESS : OpenThermResponseStatus::INVALID; + // Error msgType (READ_ACK | WRITE_ACK) is Header + if (processResponseCallback != NULL) + { + processResponseCallback(response, responseStatus); + } + } + else if (st == OpenThermStatus::DELAY) + { + if ((newTs - ts) > 100000) + { + status = OpenThermStatus::READY; + } + } +} + +bool OpenTherm::parity(unsigned long frame) // odd parity +{ + byte p = 0; + while (frame > 0) + { + if (frame & 1) + p++; + frame = frame >> 1; + } + return (p & 1); +} + +OpenThermMessageType OpenTherm::getMessageType(unsigned long message) +{ + OpenThermMessageType msg_type = static_cast((message >> 28) & 7); + return msg_type; +} + +OpenThermMessageID OpenTherm::getDataID(unsigned long frame) +{ + return (OpenThermMessageID)((frame >> 16) & 0xFF); +} + +unsigned long OpenTherm::buildRequest(OpenThermMessageType type, OpenThermMessageID id, unsigned int data) +{ + unsigned long request = data; + if (type == OpenThermMessageType::WRITE_DATA) + { + request |= 1ul << 28; + } + request |= ((unsigned long)id) << 16; + if (parity(request)) + request |= (1ul << 31); + return request; +} +unsigned long OpenTherm::buildRequestID(OpenThermMessageType type, unsigned int id, unsigned int data) +{ + unsigned long request = data; + if (type == OpenThermMessageType::WRITE_DATA) + { + request |= 1ul << 28; + } + request |= ((unsigned long)id) << 16; + if (parity(request)) + request |= (1ul << 31); + return request; +} + +unsigned long OpenTherm::buildResponse(OpenThermMessageType type, OpenThermMessageID id, unsigned int data) +{ + unsigned long response = data; + response |= ((unsigned long)type) << 28; + response |= ((unsigned long)id) << 16; + if (parity(response)) + response |= (1ul << 31); + return response; +} + +bool OpenTherm::isValidResponse(unsigned long response) +{ + if (parity(response)) + return false; + byte msgType = (response << 1) >> 29; + return msgType == READ_ACK || msgType == WRITE_ACK; +} + +bool OpenTherm::isValidRequest(unsigned long request) +{ + if (parity(request)) + return false; + byte msgType = (request << 1) >> 29; + return msgType == READ_DATA || msgType == WRITE_DATA; +} + +void OpenTherm::end() +{ + if (this->handleInterruptCallback != NULL) + { + detachInterrupt(digitalPinToInterrupt(inPin)); + } +} + +const char *OpenTherm::statusToString(OpenThermResponseStatus status) +{ + switch (status) + { + case NONE: + return "NONE"; + case SUCCESS: + return "SUCCESS"; + case INVALID: + return "INVALID"; + case TIMEOUT: + return "TIMEOUT"; + default: + return "UNKNOWN"; + } +} + +const char *OpenTherm::messageTypeToString(OpenThermMessageType message_type) +{ + switch (message_type) + { + case READ_DATA: + return "READ_DATA"; + case WRITE_DATA: + return "WRITE_DATA"; + case INVALID_DATA: + return "INVALID_DATA"; + case RESERVED: + return "RESERVED"; + case READ_ACK: + return "READ_ACK"; + case WRITE_ACK: + return "WRITE_ACK"; + case DATA_INVALID: + return "DATA_INVALID"; + case UNKNOWN_DATA_ID: + return "UNKNOWN_DATA_ID"; + default: + return "UNKNOWN"; + } +} + +// building requests + +unsigned long OpenTherm::buildSetBoilerStatusRequest(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2, bool enableSummerMode, bool dhwBlock) +{ + unsigned int data = enableCentralHeating | (enableHotWater << 1) | (enableCooling << 2) | (enableOutsideTemperatureCompensation << 3) | (enableCentralHeating2 << 4) | (enableSummerMode << 5) | (dhwBlock << 6); + data <<= 8; + return buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Status, data); +} + +unsigned long OpenTherm::buildSetBoilerTemperatureRequest(float temperature) +{ + unsigned int data = temperatureToData(temperature); + return buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TSet, data); +} + +unsigned long OpenTherm::buildGetBoilerTemperatureRequest() +{ + return buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Tboiler, 0); +} + +// parsing responses +bool OpenTherm::isFault(unsigned long response) +{ + return response & 0x1; +} + +bool OpenTherm::isCentralHeatingActive(unsigned long response) +{ + return response & 0x2; +} + +bool OpenTherm::isHotWaterActive(unsigned long response) +{ + return response & 0x4; +} + +bool OpenTherm::isFlameOn(unsigned long response) +{ + return response & 0x8; +} + +bool OpenTherm::isCoolingActive(unsigned long response) +{ + return response & 0x10; +} + +bool OpenTherm::isDiagnostic(unsigned long response) +{ + return response & 0x40; +} + +uint16_t OpenTherm::getUInt(const unsigned long response) const +{ + const uint16_t u88 = response & 0xffff; + return u88; +} + +float OpenTherm::getFloat(const unsigned long response) const +{ + const uint16_t u88 = getUInt(response); + const float f = (u88 & 0x8000) ? -(0x10000L - u88) / 256.0f : u88 / 256.0f; + return f; +} + +unsigned int OpenTherm::temperatureToData(float temperature) +{ + if (temperature < 0) + temperature = 0; + if (temperature > 100) + temperature = 100; + unsigned int data = (unsigned int)(temperature * 256); + return data; +} + +// basic requests + +unsigned long OpenTherm::setBoilerStatus(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2, bool enableSummerMode, bool dhwBlock) +{ + return sendRequest(buildSetBoilerStatusRequest(enableCentralHeating, enableHotWater, enableCooling, enableOutsideTemperatureCompensation, enableCentralHeating2, enableSummerMode, dhwBlock)); +} + +bool OpenTherm::setBoilerTemperature(float temperature) +{ + unsigned long response = sendRequest(buildSetBoilerTemperatureRequest(temperature)); + return isValidResponse(response); +} + +float OpenTherm::getBoilerTemperature() +{ + unsigned long response = sendRequest(buildGetBoilerTemperatureRequest()); + return isValidResponse(response) ? getFloat(response) : 0; +} + +float OpenTherm::getReturnTemperature() +{ + unsigned long response = sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::Tret, 0)); + return isValidResponse(response) ? getFloat(response) : 0; +} + +bool OpenTherm::setDHWSetpoint(float temperature) +{ + unsigned int data = temperatureToData(temperature); + unsigned long response = sendRequest(buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TdhwSet, data)); + return isValidResponse(response); +} + +float OpenTherm::getDHWTemperature() +{ + unsigned long response = sendRequest(buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Tdhw, 0)); + return isValidResponse(response) ? getFloat(response) : 0; +} + +float OpenTherm::getModulation() +{ + unsigned long response = sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::RelModLevel, 0)); + return isValidResponse(response) ? getFloat(response) : 0; +} + +float OpenTherm::getPressure() +{ + unsigned long response = sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::CHPressure, 0)); + return isValidResponse(response) ? getFloat(response) : 0; +} + +unsigned char OpenTherm::getFault() +{ + return ((sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::ASFflags, 0)) >> 8) & 0xff); +} +int8_t flame_timer = 0; +void OpenTherm::ImitationResponse(unsigned long request) +{ + + // unsigned long response; + unsigned int data = getUInt(request); + OpenThermMessageType msgType; + byte ID; + OpenThermMessageID id = getDataID(request); + uint8_t flags; + + switch (id) + { + case OpenThermMessageID::Status: + // Статус котла получен + msgType = OpenThermMessageType::READ_ACK; + static int8_t flame = 0; + flame_timer++; + if (flame_timer > 10) + flame = 1; + if (flame_timer > 20) + { + flame_timer = 0; + flame = 0; + } + static int8_t fault = 0; + // fault = 1 - fault; + data = (bool)fault | (true << 1) | (true << 2) | ((bool)flame << 3) | (false << 4); + break; + case OpenThermMessageID::SConfigSMemberIDcode: + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::SlaveVersion: + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::MasterVersion: + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::RelModLevel: + static float RelModLevel = 10; + // RelModLevel = RelModLevel > 100 ? 10 : RelModLevel + 1; + if (flame_timer < 11) + { + RelModLevel = 0; + } + else + { + RelModLevel = RelModLevel == 0 ? 10 : RelModLevel + 1; + } + // data = RelModLevel; + data = temperatureToData(RelModLevel); + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::Tboiler: + // Получили температуру котла + static float Tboiler = 40; + Tboiler = Tboiler > 60 ? 40 : Tboiler + 1; + data = temperatureToData(Tboiler); + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::Tdhw: + // Получили температуру ГВС + static float Tdhw = 60; + Tdhw = Tdhw > 80 ? 60 : Tdhw + 1; + data = temperatureToData(Tdhw); + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::Toutside: + // Получили внешнюю температуру + static float Toutside = -10; + Toutside = Toutside > 10 ? -10 : Toutside + 1; + data = temperatureToData(Toutside); + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::ASFflags: + msgType = OpenThermMessageType::READ_ACK; + break; + + case OpenThermMessageID::TdhwSetUBTdhwSetLB: + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::MaxTSetUBMaxTSetLB: + msgType = OpenThermMessageType::READ_ACK; + break; + + case OpenThermMessageID::OEMDiagnosticCode: + msgType = OpenThermMessageType::READ_ACK; + break; + + case OpenThermMessageID::OpenThermVersionSlave: + msgType = OpenThermMessageType::READ_ACK; + break; + + case OpenThermMessageID::CHPressure: + msgType = OpenThermMessageType::READ_ACK; + break; + + break; + case OpenThermMessageID::DHWFlowRate: + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::DayTime: + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::Date: + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::Year: + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + + case OpenThermMessageID::Tret: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::Tstorage: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::Tcollector: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::TflowCH2: + // + msgType = OpenThermMessageType::READ_ACK; + + break; + case OpenThermMessageID::Tdhw2: + // + msgType = OpenThermMessageType::READ_ACK; + + break; + case OpenThermMessageID::Texhaust: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::TheatExchanger: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::BoilerFanSpeed: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::ElectricBurnerFlame: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::BurnerStarts: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::CHPumpStarts: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWPumpValveStarts: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWBurnerStarts: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::BurnerOperationHours: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::CHPumpOperationHours: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWPumpValveOperationHours: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWBurnerOperationHours: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::RBPflags: + // + // Pre-Defined Remote Boiler Parameters + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::TdhwSet: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::TSet: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::MaxTSet: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::Hcratio: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::TSP: + // + // Transparent Slave Parameters + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::FHBsize: + // + // Fault History Data + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::MaxCapacityMinModLevel: + // + // Boiler Sequencer Control + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::TrOverride: + // + // Remote override room setpoint + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::RemoteOverrideFunction: + msgType = OpenThermMessageType::READ_ACK; + break; + + default: + msgType = OpenThermMessageType::UNKNOWN_DATA_ID; + break; + } + response = buildResponse(msgType, id, data); + status = OpenThermStatus::RESPONSE_READY; + responseStatus = OpenThermResponseStatus::SUCCESS; + /* + if (processResponseCallback != NULL) + { + processResponseCallback(response, OpenThermResponseStatus::SUCCESS); + } + */ +} \ No newline at end of file diff --git a/src/modules/exec/OpenThermSlave/OpenTherm.h b/src/modules/exec/OpenThermSlave/OpenTherm.h new file mode 100644 index 00000000..aeea5d1b --- /dev/null +++ b/src/modules/exec/OpenThermSlave/OpenTherm.h @@ -0,0 +1,208 @@ +/* +OpenTherm.h - OpenTherm Library for the ESP8266/Arduino platform +https://github.com/ihormelnyk/OpenTherm +http://ihormelnyk.com/pages/OpenTherm +Licensed under MIT license +Copyright 2018, Ihor Melnyk + +Frame Structure: +P MGS-TYPE SPARE DATA-ID DATA-VALUE +0 000 0000 00000000 00000000 00000000 +*/ + +#ifndef OpenTherm_h +#define OpenTherm_h + +#include +#include + +enum OpenThermResponseStatus : uint8_t +{ + NONE, + SUCCESS, + INVALID, + TIMEOUT +}; + +enum OpenThermMessageType : uint8_t +{ + /* Master to Slave */ + READ_DATA = B000, + READ = READ_DATA, // for backwared compatibility + WRITE_DATA = B001, + WRITE = WRITE_DATA, // for backwared compatibility + INVALID_DATA = B010, + RESERVED = B011, + /* Slave to Master */ + READ_ACK = B100, + WRITE_ACK = B101, + DATA_INVALID = B110, + UNKNOWN_DATA_ID = B111 +}; + +typedef OpenThermMessageType OpenThermRequestType; // for backwared compatibility + +enum OpenThermMessageID : uint8_t +{ + Status, // flag8 / flag8 Master and Slave Status flags. + TSet, // f8.8 Control setpoint ie CH water temperature setpoint (°C) + MConfigMMemberIDcode, // flag8 / u8 Master Configuration Flags / Master MemberID Code + SConfigSMemberIDcode, // flag8 / u8 Slave Configuration Flags / Slave MemberID Code + Command, // u8 / u8 Remote Command + ASFflags, // / OEM-fault-code flag8 / u8 Application-specific fault flags and OEM fault code + RBPflags, // flag8 / flag8 Remote boiler parameter transfer-enable & read/write flags + CoolingControl, // f8.8 Cooling control signal (%) + TsetCH2, // f8.8 Control setpoint for 2e CH circuit (°C) + TrOverride, // f8.8 Remote override room setpoint + TSP, // u8 / u8 Number of Transparent-Slave-Parameters supported by slave + TSPindexTSPvalue, // u8 / u8 Index number / Value of referred-to transparent slave parameter. + FHBsize, // u8 / u8 Size of Fault-History-Buffer supported by slave + FHBindexFHBvalue, // u8 / u8 Index number / Value of referred-to fault-history buffer entry. + MaxRelModLevelSetting, // f8.8 Maximum relative modulation level setting (%) + MaxCapacityMinModLevel, // u8 / u8 Maximum boiler capacity (kW) / Minimum boiler modulation level(%) + TrSet, // f8.8 Room Setpoint (°C) + RelModLevel, // f8.8 Relative Modulation Level (%) + CHPressure, // f8.8 Water pressure in CH circuit (bar) + DHWFlowRate, // f8.8 Water flow rate in DHW circuit. (litres/minute) + DayTime, // special / u8 Day of Week and Time of Day + Date, // u8 / u8 Calendar date + Year, // u16 Calendar year + TrSetCH2, // f8.8 Room Setpoint for 2nd CH circuit (°C) + Tr, // f8.8 Room temperature (°C) + Tboiler, // f8.8 Boiler flow water temperature (°C) + Tdhw, // f8.8 DHW temperature (°C) + Toutside, // f8.8 Outside temperature (°C) + Tret, // f8.8 Return water temperature (°C) + Tstorage, // f8.8 Solar storage temperature (°C) + Tcollector, // f8.8 Solar collector temperature (°C) + TflowCH2, // f8.8 Flow water temperature CH2 circuit (°C) + Tdhw2, // f8.8 Domestic hot water temperature 2 (°C) + Texhaust, // s16 Boiler exhaust temperature (°C) + TheatExchanger, // f8.8 Boiler heat exchanger temperature (°C) + BoilerFanSpeed, // u16 Boiler fan speed Setpiont and actual value + ElectricBurnerFlame, // f8.8?? Electric current through burner flame (mюA) + TdhwSetUBTdhwSetLB = 48, // s8 / s8 DHW setpoint upper & lower bounds for adjustment (°C) + MaxTSetUBMaxTSetLB, // s8 / s8 Max CH water setpoint upper & lower bounds for adjustment (°C) + HcratioUBHcratioLB, // s8 / s8 OTC heat curve ratio upper & lower bounds for adjustment + TdhwSet = 56, // f8.8 DHW setpoint (°C) (Remote parameter 1) + MaxTSet, // f8.8 Max CH water setpoint (°C) (Remote parameters 2) + Hcratio, // f8.8 OTC heat curve ratio (°C) (Remote parameter 3) + RemoteOverrideFunction = 100, // flag8 / - Function of manual and program changes in master and remote room setpoint. + OEMDiagnosticCode = 115, // u16 OEM-specific diagnostic/service code + BurnerStarts, // u16 Number of starts burner + CHPumpStarts, // u16 Number of starts CH pump + DHWPumpValveStarts, // u16 Number of starts DHW pump/valve + DHWBurnerStarts, // u16 Number of starts burner during DHW mode + BurnerOperationHours, // u16 Number of hours that burner is in operation (i.e. flame on) + CHPumpOperationHours, // u16 Number of hours that CH pump has been running + DHWPumpValveOperationHours, // u16 Number of hours that DHW pump has been running or DHW valve has been opened + DHWBurnerOperationHours, // u16 Number of hours that burner is in operation during DHW mode + OpenThermVersionMaster, // f8.8 The implemented version of the OpenTherm Protocol Specification in the master. + OpenThermVersionSlave, // f8.8 The implemented version of the OpenTherm Protocol Specification in the slave. + MasterVersion, // u8 / u8 Master product version number and type + SlaveVersion, // u8 / u8 Slave product version number and type +}; + +enum OpenThermStatus : uint8_t +{ + NOT_INITIALIZED, + READY, + DELAY, + REQUEST_SENDING, + RESPONSE_WAITING, + RESPONSE_START_BIT, + RESPONSE_RECEIVING, + RESPONSE_READY, + RESPONSE_INVALID +}; + +class OpenTherm +{ +public: + OpenTherm(int inPin = 4, int outPin = 5, bool isSlave = false); + volatile OpenThermStatus status; + void begin(void (*handleInterruptCallback)(void)); + void begin(void (*handleInterruptCallback)(void), void (*processResponseCallback)(unsigned long, OpenThermResponseStatus)); + bool isReady(); + unsigned long sendRequest(unsigned long request); + bool sendResponse(unsigned long request); + bool sendRequestAync(unsigned long request); + unsigned long buildRequest(OpenThermMessageType type, OpenThermMessageID id, unsigned int data); + unsigned long buildRequestID(OpenThermMessageType type, unsigned int id, unsigned int data); + unsigned long buildResponse(OpenThermMessageType type, OpenThermMessageID id, unsigned int data); + unsigned long getLastResponse(); + OpenThermResponseStatus getLastResponseStatus(); + const char *statusToString(OpenThermResponseStatus status); + void handleInterrupt(); + void process(); + void end(); + + bool parity(unsigned long frame); + OpenThermMessageType getMessageType(unsigned long message); + OpenThermMessageID getDataID(unsigned long frame); + const char *messageTypeToString(OpenThermMessageType message_type); + bool isValidRequest(unsigned long request); + bool isValidResponse(unsigned long response); + + // requests + unsigned long buildSetBoilerStatusRequest(bool enableCentralHeating, bool enableHotWater = false, bool enableCooling = false, bool enableOutsideTemperatureCompensation = false, bool enableCentralHeating2 = false, bool enableSummerMode = false, bool dhwBlock = false); + unsigned long buildSetBoilerTemperatureRequest(float temperature); + unsigned long buildGetBoilerTemperatureRequest(); + + // responses + bool isFault(unsigned long response); + bool isCentralHeatingActive(unsigned long response); + bool isHotWaterActive(unsigned long response); + bool isFlameOn(unsigned long response); + bool isCoolingActive(unsigned long response); + bool isDiagnostic(unsigned long response); + uint16_t getUInt(const unsigned long response) const; + float getFloat(const unsigned long response) const; + unsigned int temperatureToData(float temperature); + + // basic requests + unsigned long setBoilerStatus(bool enableCentralHeating, bool enableHotWater = false, bool enableCooling = false, bool enableOutsideTemperatureCompensation = false, bool enableCentralHeating2 = false, bool enableSummerMode = false, bool dhwBlock = false); + bool setBoilerTemperature(float temperature); + float getBoilerTemperature(); + float getReturnTemperature(); + bool setDHWSetpoint(float temperature); + float getDHWTemperature(); + float getModulation(); + float getPressure(); + unsigned char getFault(); + + //Имитация ответов от котла, TRUE - идет имитация ответов котла, в котел так же шлется (лучше его отключить), FALSE - штатная работа + void imitation(bool fl) {imitFlag = fl;} + +private: + bool imitFlag; + void ImitationResponse(unsigned long request); + + const int inPin; + const int outPin; + const bool isSlave; + + volatile unsigned long response; + volatile OpenThermResponseStatus responseStatus; + volatile unsigned long responseTimestamp; + volatile byte responseBitIndex; + + int readState(); + void setActiveState(); + void setIdleState(); + void activateBoiler(); + + void sendBit(bool high); + void (*handleInterruptCallback)(); + void (*processResponseCallback)(unsigned long, OpenThermResponseStatus); +}; + +#ifndef ICACHE_RAM_ATTR +#define ICACHE_RAM_ATTR +#endif + +#ifndef IRAM_ATTR +#define IRAM_ATTR ICACHE_RAM_ATTR +#endif + +#endif // OpenTherm_h diff --git a/src/modules/exec/OpenThermSlave/OpenThermSlave.cpp b/src/modules/exec/OpenThermSlave/OpenThermSlave.cpp new file mode 100644 index 00000000..59357de9 --- /dev/null +++ b/src/modules/exec/OpenThermSlave/OpenThermSlave.cpp @@ -0,0 +1,589 @@ +#include "Global.h" +#include "classes/IoTItem.h" +#include +#include "OpenTherm.h" + +#define SLAVE true +#define TIMEOUT_TRESHOLD 5 + +namespace _OpenThermSlave +{ + OpenTherm *ot_driver = nullptr; + OpenTherm *instance_OTdriver(int _RX_pin, int _TX_pin) + { + if (!ot_driver) + { + ot_driver = new OpenTherm(_RX_pin, _TX_pin, SLAVE); + // ot_driver->begin(); + } + return ot_driver; + } + + // Обработчик прерываний от ОТ + void IRAM_ATTR handleInterruptSlave() + { + if (ot_driver != nullptr) + ot_driver->handleInterrupt(); + } + + // команды/установки от термостата + struct SetpointBoiler + { + uint8_t cmd_chEnable = 0; + uint8_t cmd_dhwEnable = 0; + float TSetCH = 0; + float TSetDhw = 0; + } set; + + struct failCode + { + bool service_required = 0; + bool lockout_reset = 0; + bool low_water_pressure = 0; + bool gas_fault = 0; + bool air_fault = 0; + bool water_overtemp = 0; + uint8_t fault_code = 0; + }; + + // текущее реальное состояние котла + struct StateBoiler + { + uint8_t stateCH = 0; + uint8_t stateDHW = 0; + uint8_t fl_flame = 0; + uint8_t fl_fail = 0; + failCode fCode; + float RelModLevel = 0; + float Tboiler = 0; + float Tret = 0; + float Tdhw = 0; + float Toutside = 0; + } state; + + // конфигурация котла + struct ConfigBoiler + { + bool dhw = false; // 1- есть реле(трехходовой) ГВС + bool ctrlType = false; // 0 - модуляция, 1- вкл/выкл + bool confDhw = true; // 1 - бак, 0 - проточная //TODO ПОКА НЕ ЗНАЮ ЧТО ДЕЛАТЬ + bool pumpControlMaster = false; // в протоколе ОТ: мастер управляет насосом ????????????????????? //TODO Команды кправления насосом от мастера не помню + + int minDhw; + int maxDhw; + int minCH; + int maxCH; + + } conf; + + // DynamicJsonDocument OpenThemData(JSON_BUFFER_SIZE / 2); + + IoTItem *tmp; + + IoTItem *_idTboiler = nullptr; + IoTItem *_idTret = nullptr; + IoTItem *_idToutside = nullptr; + IoTItem *_idStateCH = nullptr; + IoTItem *_idStateDHW = nullptr; + IoTItem *_idStateFlame = nullptr; + IoTItem *_idModLevel = nullptr; + IoTItem *_idTDhw = nullptr; + IoTItem *_idCmdCH = nullptr; + IoTItem *_idCmdDHW = nullptr; + IoTItem *_idSetCH = nullptr; + IoTItem *_idSetDHW = nullptr; + IoTItem *_idCtrlType = nullptr; + + unsigned long timeout_count = 0; + + uint8_t _debug = 0; + bool _telegram = false; + unsigned long ot_response = 0; + uint8_t SlaveMemberIDcode = 0; + + + void publishNew(String widget, String value) + { + tmp = findIoTItem(widget); + if (tmp) + { + tmp->setValue(value, true); + } + else + { + if (_debug > 0) + SerialPrint("new", "SmartBoiler", widget + " = " + value); + } + } + + void sendTelegramm(String msg) + { + if (_telegram == 1) + { + if (tlgrmItem) + tlgrmItem->sendTelegramMsg(false, msg); + } + } + + /* + * ========================================================================================= + * КЛАСС РАБОТЫ ПО ПРОТОКОЛУ OPENTHERM + * ========================================================================================= + */ + class OpenThermSlave : public IoTItem + { + private: + // unsigned long ts = 0; + + + + public: + OpenThermSlave(String parameters) : IoTItem(parameters) + { + int _RX_pin = 16; + int _TX_pin = 4; + SerialPrint("i", F("OpenThermSlave"), " START... "); + + jsonRead(parameters, "RX_pin", _RX_pin); + jsonRead(parameters, "TX_pin", _TX_pin); + jsonRead(parameters, "MemberID", (int &)SlaveMemberIDcode); + + jsonRead(parameters, "LogLevel", (int &)_debug); + jsonRead(parameters, "telegram", _telegram); + + String tmpID; + jsonRead(parameters, "idTboiler", tmpID); + _idTboiler = findIoTItem(tmpID); + jsonRead(parameters, "idTret", tmpID); + _idTret = findIoTItem(tmpID); + jsonRead(parameters, "idToutside", tmpID); + _idToutside = findIoTItem(tmpID); + jsonRead(parameters, "idStateCH", tmpID); + _idStateCH = findIoTItem(tmpID); + jsonRead(parameters, "idStateDHW", tmpID); + _idStateDHW = findIoTItem(tmpID); + jsonRead(parameters, "idStateFlame", tmpID); + _idStateFlame = findIoTItem(tmpID); + jsonRead(parameters, "idModLevel", tmpID); + _idModLevel = findIoTItem(tmpID); + jsonRead(parameters, "idTDhw", tmpID); + _idTDhw = findIoTItem(tmpID); + jsonRead(parameters, "idCmdCH", tmpID); + _idCmdCH = findIoTItem(tmpID); + jsonRead(parameters, "idCmdDHW", tmpID); + _idCmdDHW = findIoTItem(tmpID); + jsonRead(parameters, "idCtrlType", tmpID); + _idCtrlType = findIoTItem(tmpID); + + jsonRead(parameters, "idSetCH", tmpID); + _idSetCH = findIoTItem(tmpID); + jsonRead(parameters, "idSetDHW", tmpID); + _idSetDHW = findIoTItem(tmpID); + + jsonRead(parameters, "minCH", conf.minCH); + jsonRead(parameters, "maxCH", conf.maxCH); + jsonRead(parameters, "minDhw", conf.minDhw); + jsonRead(parameters, "maxDhw", conf.maxDhw); + + instance_OTdriver(_RX_pin, _TX_pin); + ot_driver->begin(handleInterruptSlave, processRequest); // responseCallback +// ot_boiler = this; + } + + void doByInterval() + { + } + + // Основной цикл программы + void loop() + { + ot_driver->process(); + IoTItem::loop(); + } + + // Комманды из сценария + IoTValue execute(String command, std::vector ¶m) + { + return {}; + } + + // Обработка управления и отправка статуса + static void processStatus(unsigned int &data) + { + + uint8_t statusRequest = data >> 8; // забрали старший байт с командами мастера + set.cmd_chEnable = statusRequest & 0x1; // забрали 0 бит из этого байта - включение СО (маска 01) + + set.cmd_dhwEnable = statusRequest & 0x2; // забрали 1 бит из этого байта - включение СО (маска 10) + IoTValue val; + val.valD = set.cmd_chEnable; + _idCmdCH->setValue(val, true); + val.valD = set.cmd_dhwEnable; + _idCmdDHW->setValue(val, true); + data &= 0xFF00; // старший бит не трогаем, а младший обнулили, что бы его заполнить состоянием котла и вернуть data термостату + + // if (_idFail) + // state.fl_fail = ::atof(_idFail->getValue().c_str()); + if (_idStateCH) + state.stateCH = ::atoi(_idStateCH->getValue().c_str()); + if (_idStateDHW) + state.stateDHW = ::atoi(_idStateDHW->getValue().c_str()); + if (_idStateFlame) + state.fl_flame = ::atoi(_idStateFlame->getValue().c_str()); + + if (state.fl_fail) + data |= 0x01; // fault indication + if (state.stateCH) + data |= 0x02; // CH active + if (state.stateDHW) + data |= 0x04; // DHW active + if (state.fl_flame) + data |= 0x08; // flame on + // data |= 0x10; //cooling active + // data |= 0x20; //CH2 active + // data |= 0x40; //diagnostic/service event + // data |= 0x80; //electricity production on + } + + // обработка сброса ошибок + static void processCommand(unsigned int &data) + { + uint8_t command = data >> 8; // забрали старший байт с командами мастера + if (command == 1) + { + state.fl_fail = 0; + data |= 128; // ответ 128-255: команда выполнена + } + } + + //=================================== Обработка входящих сообщение ОТ ====================================== + static void processRequest(unsigned long request, OpenThermResponseStatus status) + { + switch (status) + { + case OpenThermResponseStatus::NONE: + if (_debug > 0) + { + SerialPrint("E", "OpenThermSlave", "Error: OpenTherm не инициализирован"); + } + break; + case OpenThermResponseStatus::INVALID: + if (_debug > 0) + { + SerialPrint("E", "OpenThermSlave", "ID:" + String(ot_driver->getDataID(request)) + " / Error: Ошибка разбора команды: " + String(request, HEX)); + // build UNKNOWN-DATAID response + unsigned long response = ot_driver->buildResponse(OpenThermMessageType::DATA_INVALID, ot_driver->getDataID(request), 0); + + // send response + ot_driver->sendResponse(response); + } + break; + case OpenThermResponseStatus::TIMEOUT: + if (_debug > 0) + { + SerialPrint("E", "OpenThermSlave", " ID: " + String(ot_driver->getDataID(request)) + " / Error: Таймаут команд от управляющего устройства (термостата)"); + } + timeout_count++; + if (timeout_count > TIMEOUT_TRESHOLD) + { + publishNew("boilerslave", "❌"); + // publishNew("status", "не подключен"); + timeout_count = TIMEOUT_TRESHOLD; + sendTelegramm(("OpenTherm: потеря связи с управляющим устройством (термостатом) ❌")); + } + break; + case OpenThermResponseStatus::SUCCESS: + timeout_count = 0; + publishNew("boilerslave", "✅"); + // publishNew("status", "подключен"); + // sendTelegramm(("OpenTherm: котёл подключен ✅")); + // respondense_flag = true; + // ts_ = new_ts_; + HandleRequest(request); + break; + default: + break; + } + } + + // Парсинг запросов + static void HandleRequest(unsigned long request) + { + if (_idCtrlType) + conf.ctrlType = ::atoi(_idCtrlType->getValue().c_str()); + // unsigned long response; + unsigned int data = ot_driver->getUInt(request); + OpenThermMessageType msgType; + byte ID; + OpenThermMessageID id = ot_driver->getDataID(request); + uint8_t flags; + if (_debug > 2) + { + SerialPrint("i", "OpenThermSlave <-", String(millis()) + " ID: " + String(id) + " / requestHEX: " + String(request, HEX) + " / request: " + String(request)); + } + switch (id) + { + /*----------------------------Инициализация и конфигурация----------------------------*/ + case OpenThermMessageID::SConfigSMemberIDcode: // запрос Конфигурации Котла и SlaveMemberID + msgType = OpenThermMessageType::READ_ACK; + data = conf.dhw | (conf.ctrlType << 1) | (false << 2) | (conf.confDhw << 3) | (conf.pumpControlMaster << 4) | (false << 5); // 2-cooling, 5-CH2 + data <<= 8; + data |= SlaveMemberIDcode; + // data = (int)SlaveMemberIDcode; + break; + // case OpenThermMessageID::MConfigMMemberIDcode: // Получили Master Member ID + // msgType = OpenThermMessageType::WRITE_ACK; + // break; + // case OpenThermMessageID::SlaveVersion: // TODO вернуть версию модуля + // msgType = OpenThermMessageType::READ_ACK; + // data = (int)1; + // break; + // case OpenThermMessageID::MasterVersion: + // msgType = OpenThermMessageType::WRITE_ACK; + // break; + // case OpenThermMessageID::OpenThermVersionSlave: + // msgType = OpenThermMessageType::READ_ACK; + // break; + /*----------------------------Управление (уставки и команды)----------------------------*/ + case OpenThermMessageID::TdhwSetUBTdhwSetLB: // границы уставки ГВС + msgType = OpenThermMessageType::READ_ACK; + data |= (uint8_t)conf.minDhw; + data |= (uint8_t)conf.maxDhw << 8; + break; + case OpenThermMessageID::MaxTSetUBMaxTSetLB: // границы уставки СО + msgType = OpenThermMessageType::READ_ACK; + data |= (uint8_t)conf.minCH; + data |= (uint8_t)conf.maxCH << 8; + break; + case OpenThermMessageID::Command: // Сброс ошибок/сброс блокировки котла. Ответ: команды (не)выполнена + msgType = OpenThermMessageType::READ_ACK; + processCommand(data); + break; + case OpenThermMessageID::TdhwSet: // TODO Получили температуру ГВС + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + { + msgType = OpenThermMessageType::READ_ACK; + if (_idSetDHW) + set.TSetDhw = ::atof(_idSetDHW->getValue().c_str()); + data = ot_driver->temperatureToData(set.TSetDhw); + } + else + { + msgType = OpenThermMessageType::WRITE_ACK; + // processDHWSet(ot_driver->getFloat(data)); + set.TSetDhw = ot_driver->getFloat(data); + set.TSetDhw = constrain(set.TSetDhw, conf.minDhw, conf.maxDhw); + // publishNew("TDHWSet", String(set.TSetDhw)); + IoTValue val; + val.valD = set.TSetDhw; + _idSetDHW->setValue(val, true); + } + break; + case OpenThermMessageID::TSet: // TODO Получили температуру СО + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + { + msgType = OpenThermMessageType::READ_ACK; + if (_idSetCH) + set.TSetCH = ::atof(_idSetCH->getValue().c_str()); + data = ot_driver->temperatureToData(set.TSetCH); + } + else + { + msgType = OpenThermMessageType::WRITE_ACK; + // processCHSet(ot_driver->getFloat(data)); + set.TSetCH = ot_driver->getFloat(data); + set.TSetCH = constrain(set.TSetCH, conf.minCH, conf.maxCH); + // publishNew("TCHSet", String(set.TSetCH)); + IoTValue val; + val.valD = set.TSetCH; + _idSetCH->setValue(val, true); + } + break; + /* case OpenThermMessageID::MaxTSet: // максимальная уставка ГВС ?????? + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::Hcratio: // Коэффециент тепловой кривой + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + */ + /*----------------------------Состояние и статусы----------------------------*/ + case OpenThermMessageID::Status: // TODO Вернуть Статус котла + msgType = OpenThermMessageType::READ_ACK; + processStatus(data); + break; + case OpenThermMessageID::RelModLevel: // запрос модуляции + msgType = OpenThermMessageType::READ_ACK; + if (_idModLevel) + state.RelModLevel = ::atoi(_idModLevel->getValue().c_str()); + data = ot_driver->temperatureToData(state.RelModLevel); + break; + case OpenThermMessageID::Tboiler: // запрос температуры котла + msgType = OpenThermMessageType::READ_ACK; + if (_idTboiler) + state.Tboiler = ::atof(_idTboiler->getValue().c_str()); + data = ot_driver->temperatureToData(state.Tboiler); + break; + case OpenThermMessageID::Tdhw: // запрос температуры ГВС + msgType = OpenThermMessageType::READ_ACK; + if (_idTDhw) + { + state.Tdhw = ::atof(_idTDhw->getValue().c_str()); + data = ot_driver->temperatureToData(state.Tdhw); + } + else + { + msgType = OpenThermMessageType::UNKNOWN_DATA_ID; + } + break; + case OpenThermMessageID::Toutside: // запрос внешней температуры + msgType = OpenThermMessageType::READ_ACK; + if (_idToutside) + { + state.Toutside = ::atof(_idToutside->getValue().c_str()); + data = ot_driver->temperatureToData(state.Toutside); + } + else + { + msgType = OpenThermMessageType::UNKNOWN_DATA_ID; + } + break; + case OpenThermMessageID::ASFflags: // запрос ошибок + msgType = OpenThermMessageType::READ_ACK; + data = 0; + if (state.fl_fail) + { + data = state.fCode.service_required | (state.fCode.lockout_reset << 1) | (state.fCode.low_water_pressure << 2) | (state.fCode.gas_fault << 3) | (state.fCode.air_fault << 4) | (state.fCode.water_overtemp << 5); + data |= (uint8_t)state.fCode.fault_code << 8; + } + break; + case OpenThermMessageID::Tret: // температура обратки + msgType = OpenThermMessageType::READ_ACK; + if (_idTret) + { + state.Tret = ::atof(_idTret->getValue().c_str()); + data = ot_driver->temperatureToData(state.Tret); + } + else + { + msgType = OpenThermMessageType::UNKNOWN_DATA_ID; + } + break; + // case OpenThermMessageID::OEMDiagnosticCode: + // msgType = OpenThermMessageType::READ_ACK; + // break; + // case OpenThermMessageID::ElectricBurnerFlame: // Ток работы горелки ????? + // msgType = OpenThermMessageType::READ_ACK; + // break; + // case OpenThermMessageID::MaxCapacityMinModLevel: // максимальная мощность котла кВт и минимальная модуляция % + // msgType = OpenThermMessageType::READ_ACK; + // break; + + /*----------------------------Двусторонние информационные сообщения----------------------------*/ + /* case OpenThermMessageID::DayTime: + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::Date: + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::Year: + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + // ========>>>>>>>>>>> СБРОС КОЛИЧЕСТВА 0 от мастера + case OpenThermMessageID::BurnerStarts: // Количество стартов горелки + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::CHPumpStarts: // Количество стартов насоса СО + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWPumpValveStarts: // Количество стартов насоса/клапана ГВС + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWBurnerStarts: // Количество стартов горелки ГВС + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::BurnerOperationHours: // часы работы горелки + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::CHPumpOperationHours: // часы работы горелки СО + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWPumpValveOperationHours: // часы работы насоса/клапана ГВС + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWBurnerOperationHours: // часы работы горелки ГВС + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + */ + /*------------------------------------ ВСЁ ------------------------------------*/ + + default: + msgType = OpenThermMessageType::UNKNOWN_DATA_ID; + break; + } + ot_response = ot_driver->buildResponse(msgType, id, data); + ot_driver->sendResponse(ot_response); + if (_debug > 2) + { + SerialPrint("i", "OpenThermSlave ->", String(millis()) + " ID: " + String(id) + " / responseHEX: " + String(ot_response, HEX) + " / response: " + String(ot_response)); + } + } + + ~OpenThermSlave() + { + delete ot_driver; + ot_driver = nullptr; + } + }; +} + + +void *getAPI_OpenThermSlave(String subtype, String param) +{ + if (subtype == F("OpenThermSlave")) + { + return new _OpenThermSlave::OpenThermSlave(param); + } + else + { + return nullptr; + } +} diff --git a/src/modules/exec/OpenThermSlave/modinfo.json b/src/modules/exec/OpenThermSlave/modinfo.json new file mode 100644 index 00000000..2125a60f --- /dev/null +++ b/src/modules/exec/OpenThermSlave/modinfo.json @@ -0,0 +1,111 @@ +{ + "menuSection": "executive_devices", + "configItem": [ + { + "global": 0, + "name": "OpenThermSlave", + "type": "Reading", + "subtype": "OpenThermSlave", + "id": "otslave", + "widget": "", + "page": "Boiler", + "descr": "Котёл", + "int": 60, + "value": "...", + "RX_pin": 13, + "TX_pin": 15, + "LogLevel": 0, + "telegram": 1, + "MemberID": 0, + "confDhw":0, + "minCH": 35, + "maxCH": 85, + "minDhw": 30, + "maxDhw": 60, + "idTboiler": "Tboiler", + "idTret": "Tret", + "idToutside": "Toutside", + "idTDhw":"TDhw", + "idStateCH":"StateCH", + "idStateDHW":"StateDHW", + "idStateFlame":"StateFlame", + "idModLevel":"ModLevel", + "idCmdCH":"CmdCH", + "idCmdDHW":"CmdDHW", + "idSetCH":"SetCH", + "idSetDHW":"SetDHW", + "idCtrlType":"CtrlType" + } + ], + "about": { + "authorName": "Mikhail Bubnov", + "authorContact": "https://t.me/Mit4bmw", + "authorGit": "https://github.com/Mit4el", + "specialThanks": "", + "moduleName": "OpenThermSlave", + "moduleVersion": "0.1", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "OpenThermSlave", + "moduleDesc": "Модуль для автоматизации электрического котла. Мозги котла с внешним протоколом opentherm", + "propInfo": { + "int": "Интервал отправки данных в MQTT и web интерфейс", + "telegram": "1- Будет отправлять в телеграмм оповещения при ошибках котла и пропаже сигнала от котла, остальные необходимо реализовывать через сценарий", + "MemberID": "SlaveMemberIDcode - код производителя котла, кем притворится котёл;) Менять в большинстве случаев не надо", + "idPID":"ID модуля ПИД регулятора, для расчета модуляции и включения тэнов в зависимости от температуры теплоносителя, в модуле TCHSet будет уставка СО, создать TCHSet и указать его в модуле ПИД", + "idTboiler": "ID датчика температуры подачи котла", + "idTret": "ID датчика температуры обратки котла", + "idToutside": "ID датчика уличной температуры", + "rele1_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele1", + "rele2_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele2, если нет, то 0 (ноль)", + "rele3_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele3, если нет, то 0 (ноль)", + "Pupm": "1-есть реле насоса (ID реле должно называться relePump), 0-нет реле насоса, насос управляется котлом без нас", + "minCH": "Граница установки температуры СО", + "maxCH": "Граница установки температуры СО", + "gistCH": "Гистерезис СО - нагрев СО включится если температура теплоносителя ниже уставки на указанные градусы (CHSet = 45гр, gistCH = 5гр, нагрев включится когда idTboiler = 40гр)", + "idTdhw": "ID датчика температуры ГВС, например в датчик в БКН", + "idReleDhw":"ID реле трехходового крана ГВС", + "gistDhw": "Гистерезис ГВС - нагрев ГВС включится если температура воды ниже уставки на указанные градусы", + "minDhw": "Граница установки температуры ГВС", + "maxDhw": "Граница установки температуры ГВС" + }, + "funcInfo": [ + { + "name": "CHSet", + "descr": "Установить целевую температуру СО", + "params": [ + "тепмература СО (подачи) - bolier.CHSet(60)" + ] + }, + { + "name": "CHEnable", + "descr": "включить / выключить отопление", + "params": [ + "bolier.CHEnable(1) - вкл, bolier.CHEnable(0) - выкл, " + ] + }, + { + "name": "DHWSet", + "descr": "Установить целевую температуру ГВС", + "params": [ + "тепмература ГВС - dhw.DHWSet(40)" + ] + }, + { + "name": "DHWEnable", + "descr": "включить / выключить ГВС", + "params": [ + "dhw.DHWEnable(1) - вкл, dhw.DHWEnable(0) - выкл, " + ] + } + ] + }, + "defActive": true, + "usedLibs": { + "esp32_4mb3f": [], + "esp32*": [], + "esp82*": [] + } +} \ No newline at end of file diff --git a/src/modules/exec/SmartBoiler/BoilerHeader.h b/src/modules/exec/SmartBoiler/BoilerHeader.h new file mode 100644 index 00000000..cda61395 --- /dev/null +++ b/src/modules/exec/SmartBoiler/BoilerHeader.h @@ -0,0 +1,78 @@ +#pragma once + +#define SLAVE true +#define TIMEOUT_TRESHOLD 5 + +namespace _Boiler +{ + // команды/установки от термостата + struct SetpointBoiler + { + uint8_t cmd_chEnable = 0; + uint8_t cmd_dhwEnable = 0; + float TSetCH = 0; + float TSetDhw = 0; + } set; + + struct failCode + { + bool service_required = 0; + bool lockout_reset = 0; + bool low_water_pressure = 0; + bool gas_fault = 0; + bool air_fault = 0; + bool water_overtemp = 0; + uint8_t fault_code = 0; + }; + + // текущее реальное состояние котла + struct StateBoiler + { + uint8_t stateCH = 0; + uint8_t stateDHW = 0; + uint8_t fl_flame = 0; + uint8_t currentRele = 0; + uint8_t fl_fail = 0; + failCode fCode; + float RelModLevel = 0; + float Tboiler = -40; + float Tret = 0; + float Tdhw = 0; + float Toutside = 0; + bool r[3] = {0, 0, 0}; + } state; + + // конфигурация котла + struct ConfigBoiler + { + int antiFreez; + bool pump = false; // 1- наличие реле насоса СО, 0 - мы не управляем насосом СО (в протоколе ОТ нет) + bool changeRele = false; + // bool dhw = false; // 1- есть реле(трехходовой) ГВС + bool ctrlType = false; // 0 - модуляция, 1- вкл/выкл + bool confDhw = false; // 1 - бак, 0 - проточная //TODO ПОКА НЕ ЗНАЮ ЧТО ДЕЛАТЬ + bool pumpControlMaster = false; // в протоколе ОТ: мастер управляет насосом ????????????????????? //TODO Команды кправления насосом от мастера не помню + + int minDhw; + int maxDhw; + int minCH; + int maxCH; + + int gistDhw; + int gistCH; + + int countRele = 0; + int relePwr[3]={0,0,0}; + int prcOnekWt = 0; // процент одного киловата из общей мощности всех тэнев, расчитывается для модуляции + // int rele2Pwr = 0; + // int rele3Pwr = 0; + } conf; + + + + unsigned long timeout_count = 0; + + int _debug = 0; + bool _telegram = false; + unsigned long ot_response = 0; +} diff --git a/src/modules/exec/SmartBoiler/SmartBoiler.cpp b/src/modules/exec/SmartBoiler/SmartBoiler.cpp new file mode 100644 index 00000000..15775302 --- /dev/null +++ b/src/modules/exec/SmartBoiler/SmartBoiler.cpp @@ -0,0 +1,582 @@ +#include "Global.h" +#include "classes/IoTItem.h" +#include +#include "BoilerHeader.h" + +namespace _Boiler +{ + DynamicJsonDocument OpenThemData(JSON_BUFFER_SIZE / 2); + + IoTItem *_idPID = nullptr; + IoTItem *_idTboiler = nullptr; + IoTItem *_idTret = nullptr; + IoTItem *_idToutside = nullptr; + + IoTItem *_idStateCH = nullptr; + IoTItem *_idStateFlame = nullptr; + IoTItem *_idModLevel = nullptr; + IoTItem *_idCmdCH = nullptr; + IoTItem *_idSetCH = nullptr; + IoTItem *_idCtrlType = nullptr; + + int pid; + IoTItem *rele[3]; + IoTItem *relePump; + IoTItem *releDhw; + IoTItem *tmp; + + // проверяем если пришедшее значение отличается от предыдущего регистрируем событие + void publishNew(String widget, String value) + { + if (OpenThemData[widget] != value) + { + OpenThemData[widget] = value; + + tmp = findIoTItem(widget); + if (tmp) + { + tmp->setValue(value, true); + } + else + { + if (_debug > 0) + SerialPrint("new", "SmartBoiler", widget + " = " + value); + } + } + } + + void sendTelegramm(String msg) + { + if (_telegram == 1) + { + if (tlgrmItem) + tlgrmItem->sendTelegramMsg(false, msg); + } + } + + /* + * ========================================================================================= + * КЛАСС УПРАВЛЕНИЯ ГВС + * ========================================================================================= + */ + + IoTItem *_idTdhw = nullptr; + IoTItem *_idStateDHW = nullptr; + IoTItem *_idCmdDHW = nullptr; + IoTItem *_idTDhw = nullptr; + IoTItem *_idSetDHW = nullptr; + + class DHWControl : public IoTItem + { + private: + unsigned long ts = 0; + + public: + DHWControl(String parameters) : IoTItem(parameters) + { + SerialPrint("i", F("DHWControl"), " START... "); + + String tmpID; + jsonRead(parameters, "idReleDhw", tmpID); + releDhw = findIoTItem(tmpID); + + jsonRead(parameters, "idSetDHW", tmpID); + _idSetDHW = findIoTItem(tmpID); + jsonRead(parameters, "idStateDHW", tmpID); + _idStateDHW = findIoTItem(tmpID); + jsonRead(parameters, "idCmdDHW", tmpID); + _idCmdDHW = findIoTItem(tmpID); + jsonRead(parameters, "idTDhw", tmpID); + _idTDhw = findIoTItem(tmpID); + + jsonRead(parameters, "minDhw", conf.minDhw); + jsonRead(parameters, "maxDhw", conf.maxDhw); + + jsonRead(parameters, "gistDhw", conf.gistDhw); + + // releDhw = findIoTItem("releDhw"); + if (releDhw) + { + SerialPrint("i", "DHWControl", "Инициализировано РЕЛЕ ГВС"); + } + + // dhw_ctrl = this; + } + + // ============================== ЛОГИКА РАБОТЫ КОТЛА И ВКЛЮЧЕНИЯ ТЭНОВ ====================================== + + // Работы котла и включения тэнов + static void logicPowerDhw() + { + if (_idCmdDHW) + set.cmd_dhwEnable = ::atoi(_idCmdDHW->getValue().c_str()); + + if (_idTDhw) + state.Tdhw = ::atof(_idTDhw->getValue().c_str()); + + if (_idSetDHW) + set.TSetDhw = ::atof(_idSetDHW->getValue().c_str()); + + if (set.cmd_dhwEnable) + { + if (releDhw) + { + if (_idTDhw) + state.Tdhw = ::atof(_idTDhw->getValue().c_str()); + + if (set.TSetDhw - state.Tdhw >= conf.gistDhw && state.Tdhw > -5 && state.Tdhw < 120) + { + // включаем ГВС + releDhw->setValue("1", true); + state.stateDHW = 1; + state.stateCH = 0; + for (uint8_t i = 0; i < conf.countRele; i++) + { + state.r[i] = true; + } + state.RelModLevel = 100; + } + else + { + releDhw->setValue("0", true); + state.stateDHW = 0; + state.RelModLevel = 0; + } + } + } + else + { + if (releDhw) + releDhw->setValue("0", true); + } + // publishNew("StateDHW", String(state.stateDHW)); + IoTValue val; + if (_idStateDHW) + { + val.valD = state.stateDHW; + _idStateDHW->setValue(val, true); + } + } + + void doByInterval() + { + } + + // Комманды из сценария + IoTValue execute(String command, std::vector ¶m) + { + IoTValue val; + if (command == "SetDHW") + { + set.TSetDhw = param[0].valD; + set.TSetDhw = constrain(set.TSetDhw, conf.minDhw, conf.maxDhw); + // publishNew("SetDHW", String(set.TSetDhw)); + val.valD = set.TSetDhw; + if (_idSetDHW) + _idSetDHW->setValue(val, true); + SerialPrint("i", "DHWControl", "Scenario DHWSet "); + } + else if (command == "DHWEnable") + { + set.cmd_dhwEnable = param[0].valD; + val.valD = set.cmd_dhwEnable; + if (_idCmdDHW) + _idCmdDHW->setValue(val, true); + SerialPrint("i", "DHWControl", "Scenario DHWEnable "); + } + + return {}; + } + + ~DHWControl() + { + } + }; + + /* + * ========================================================================================= + * КЛАСС УПРАВЛЕНИЯ КОТЛОМ + * ========================================================================================= + */ + class BoilerControl : public IoTItem + { + private: + unsigned long ts = 0; + + public: + BoilerControl(String parameters) : IoTItem(parameters) + { + SerialPrint("i", F("BoilerControl"), " START... "); + + jsonRead(parameters, "LogLevel", _debug); + jsonRead(parameters, "telegram", _telegram); + String tmpID; + jsonRead(parameters, "idPID", tmpID); + _idPID = findIoTItem(tmpID); + jsonRead(parameters, "idTboiler", tmpID); + _idTboiler = findIoTItem(tmpID); + jsonRead(parameters, "idTret", tmpID); + _idTret = findIoTItem(tmpID); + jsonRead(parameters, "idToutside", tmpID); + _idToutside = findIoTItem(tmpID); + + jsonRead(parameters, "idStateCH", tmpID); + _idStateCH = findIoTItem(tmpID); + jsonRead(parameters, "idStateFlame", tmpID); + _idStateFlame = findIoTItem(tmpID); + jsonRead(parameters, "idModLevel", tmpID); + _idModLevel = findIoTItem(tmpID); + jsonRead(parameters, "idCmdCH", tmpID); + _idCmdCH = findIoTItem(tmpID); + jsonRead(parameters, "idSetCH", tmpID); + _idSetCH = findIoTItem(tmpID); + jsonRead(parameters, "idCtrlType", tmpID); + _idCtrlType = findIoTItem(tmpID); + + jsonRead(parameters, "rele1_Pwr", conf.relePwr[0]); + jsonRead(parameters, "rele2_Pwr", conf.relePwr[1]); + jsonRead(parameters, "rele3_Pwr", conf.relePwr[2]); + jsonRead(parameters, "Pump", conf.pump); + jsonRead(parameters, "changeRele", conf.changeRele); + + jsonRead(parameters, "minCH", conf.minCH); + jsonRead(parameters, "maxCH", conf.maxCH); + jsonRead(parameters, "gistCH", conf.gistCH); + jsonRead(parameters, "antiFreez", conf.antiFreez); + + configuration(); + } + + // ============================== ЛОГИКА РАБОТЫ КОТЛА И ВКЛЮЧЕНИЯ ТЭНОВ ====================================== + + // Работы котла и включения тэнов + static void logicPowerOn() + { + // TODO ВКЛЮЧЕНИЕ РЕЛЕ ПО ИХ МОЩНОСТИ (СДЕЛАТЬ ШАГИ НАГРЕВА И КОМБИНАЦИИ ВКЛБЧЕНИЯ ТЕНОВ С РАЗНОЙ МОЩНОСТЬЮ) + for (uint8_t i = 0; i < conf.countRele; i++) + { + state.r[i] = false; + } + // сейчас включаются по порядку + state.RelModLevel = 0; + pid = 0; + state.fl_flame = false; + if (_idPID) + pid = ::atoi(_idPID->getValue().c_str()); + // обнуляем ГВС + state.stateDHW = 0; + + if (state.Tboiler < conf.maxCH) + { + // if (dhw_ctrl) + //{ + // если есть модуль ГВС, то вызываем его логику включения тэнов + DHWControl::logicPowerDhw(); + //} + if (!state.stateDHW) // Если уже включено ГВС, то нечего смотреть на отопление + { + if (set.cmd_chEnable) + { + publishNew("status", "Штатный режим"); + // включаем отопление + state.stateCH = 1; + if (state.Tboiler < (set.TSetCH + conf.gistCH) && /*set.TSetCH - state.Tboiler >= conf.gistCH &&*/ state.Tboiler > -5 && state.Tboiler < 120) + { + if (conf.ctrlType == 0) + { // режим модуляции, это когда есть модуль ПИД и более одного реле + + if (pid > 0) + { + state.r[state.currentRele] = true; + state.RelModLevel = conf.prcOnekWt * conf.relePwr[state.currentRele]; + } + if (pid > 30 && pid <= 60) + { + uint8_t next = state.currentRele + 1; + if (next >= conf.countRele) + next = 0; + state.r[next] = true; + state.RelModLevel = (conf.prcOnekWt * conf.relePwr[state.currentRele]) + (conf.prcOnekWt * conf.relePwr[next]); + } + if (pid > 60) + { + for (uint8_t i = 0; i < conf.countRele; i++) + { + state.r[i] = true; + } + state.RelModLevel = 100; + } + } + else + { // у нас релейный режим без ПИДа или с одним реле(тэном) + // TODO СЕЙЧАС ВКЛЮЧАЕМ ВСЕ РЕЛЕ, НАДО ЧТО ТО ПРИДУМАТЬ УМНОЕ + for (uint8_t i = 0; i < conf.countRele; i++) + { + state.r[i] = true; + } + state.RelModLevel = 100; + } + } + else + { + state.RelModLevel = 0; + } + } + else + { + publishNew("status", "Выкл отопление"); + state.stateCH = 0; + } + } + } + if (conf.antiFreez > 0 && state.Tboiler < (conf.antiFreez + 4)) + { + state.r[state.currentRele] = true; + state.RelModLevel = conf.prcOnekWt * conf.relePwr[state.currentRele]; + // setValue("Анти-Заморозка"); + publishNew("status", "Режим анти-заморозка"); + SerialPrint("i", "BoilerControl", "Режим анти-заморозка"); + sendTelegramm("Режим анти-заморозка"); + } + + static bool prev_flame = false; + + if (state.RelModLevel > 0) + state.fl_flame = true; // если хоть одно реле включено, то выставляем флаг горелки + + if (state.fl_flame && prev_flame != state.fl_flame) + { + if (conf.changeRele) + { + state.currentRele++; + if (state.currentRele >= conf.countRele) + state.currentRele = 0; + } + } + prev_flame = state.fl_flame; + + bool fl_pump = false; + + // переключаем реле в соответсии с их статусами + for (uint8_t i = 0; i < conf.countRele; i++) + { + if (rele[i]) + { + rele[i]->setValue(state.r[i] ? "1" : "0", true); + } + if (!fl_pump) // что бы не обнулить если выстиавили true + fl_pump = state.r[i] ? true : false; // если хоть одно реле включено, то включаем насос + } + if (fl_pump) + { // если хоть одно реле включено, то включаем насос + if (relePump) + relePump->setValue("1", true); + } + else + { + if (relePump) + { // если все реле выключены + if (state.Tboiler > conf.minCH && set.cmd_chEnable) // НО температура ещё горячая и при этом отопление включено, то включаем насос + relePump->setValue("1", true); + else // Если температура в котле уже остыла, или отопление нам не нужно (летом нагрели воду-пусть котел сам остывает без насоса), то выключаем насос + relePump->setValue("0", true); + } + } + IoTValue val; + if (_idModLevel) + { + val.valD = state.RelModLevel; + _idModLevel->setValue(val, true); + } + if (_idStateCH) + { + val.valD = state.stateCH; + _idStateCH->setValue(val, true); + } + if (_idStateFlame) + { + val.valD = state.fl_flame; + _idStateFlame->setValue(val, true); + } + + // publishNew("ModLevel", String(state.RelModLevel)); + // publishNew("stateCH", String(state.stateCH)); + // publishNew("controlType", String(conf.ctrlType)); + // publishNew("StateFlame", String(state.fl_flame)); + } + + //============================== ОБЕСПЕЧЕНИЕ РАБОТЫ IoTMANAGER ===================================== + + // конфигурирование котла в зависимости от настроек + void configuration() + { + state.fl_flame = state.stateCH = 0; + conf.countRele = conf.prcOnekWt = 0; + if (conf.relePwr[0]) + { + rele[0] = findIoTItem("rele1"); + if (rele[0]) + { + conf.countRele++; + SerialPrint("i", "BoilerControl", "Инициализировано РЕЛЕ 1-го тэна"); + } + } + + if (conf.relePwr[1]) + { + rele[1] = findIoTItem("rele2"); + if (rele[1]) + { + conf.countRele++; + SerialPrint("i", "BoilerControl", "Инициализировано РЕЛЕ 2-го тэна"); + } + } + + if (conf.relePwr[2]) + { + rele[2] = findIoTItem("rele3"); + if (rele[2]) + { + conf.countRele++; + SerialPrint("i", "BoilerControl", "Инициализировано РЕЛЕ 3-го тэна"); + } + } + + for (int i = 0; i < conf.countRele; i++) + { + conf.prcOnekWt += conf.relePwr[i]; + } + if (conf.countRele && conf.prcOnekWt) + { + // conf.prcOnekWt /= conf.countRele; + conf.prcOnekWt = 100 / conf.prcOnekWt; + // SerialPrint("i", "BoilerControl", "Процент одного кВт = " + String (conf.prcOnekWt) + "%"); + } + + if (conf.pump) + { + relePump = findIoTItem("relePump"); + if (relePump) + { + SerialPrint("i", "BoilerControl", "Инициализировано РЕЛЕ Насоса"); + } + } + conf.ctrlType = false; + if (conf.countRele == 1 || _idPID == nullptr) + conf.ctrlType = true; + + // IoTValue val; + // val.valD = conf.ctrlType; + if (_idCtrlType) + { + if (conf.ctrlType) + _idCtrlType->setValue("Вкл/Выкл", true); + else + _idCtrlType->setValue("Модуляция", true); + } + + updateStateboiler(); + } + + void doByInterval() + { + // updateStateboiler(); + + // Принудительно чистим данные, что бы обновился интерфейс + OpenThemData.clear(); + + if (_debug > 0) + { + SerialPrint("i", "BoilerControl", "Обновляем данные в web интерфейсе"); + } + if (_debug > 0) + { + SerialPrint("i", "BoilerControl", "memoryUsage: " + String(OpenThemData.memoryUsage())); + } + } + + // Основной цикл программы + void loop() + { + unsigned long new_ts = millis(); + int delay = 1000; + if (new_ts - ts > delay) + { + ts = new_ts; + updateStateboiler(); + logicPowerOn(); + } + // для новых версий IoTManager + IoTItem::loop(); + } + + // Комманды из сценария + IoTValue execute(String command, std::vector ¶m) + { + IoTValue val; + if (command == "CHSet") + { + set.TSetCH = param[0].valD; + set.TSetCH = constrain(set.TSetCH, conf.minCH, conf.maxCH); + val.valD = set.TSetCH; + if (_idSetCH) + _idSetCH->setValue(val, true); + // publishNew("SetCH", String(set.TSetCH)); + SerialPrint("i", "BoilerControl", "Scenario CHSet "); + } + else if (command == "CHEnable") + { + set.cmd_chEnable = param[0].valD; + val.valD = set.cmd_chEnable; + if (_idCmdCH) + _idCmdCH->setValue(val, true); + SerialPrint("i", "BoilerControl", "Scenario CHEnable "); + } + + return {}; + } + + // обновление данных от датчиков + void updateStateboiler() + { + if (_idTboiler) + state.Tboiler = ::atof(_idTboiler->getValue().c_str()); + if (_idTret) + state.Tret = ::atof(_idTret->getValue().c_str()); + if (_idToutside) + state.Toutside = ::atof(_idToutside->getValue().c_str()); + + // if (_idStateCH) + // state.stateCH = ::atoi(_idStateCH->getValue().c_str()); + // if (_idStateFlame) + // state.fl_flame = ::atoi(_idStateFlame->getValue().c_str()); + // if (_idModLevel) + // state.RelModLevel = ::atof(_idModLevel->getValue().c_str()); + if (_idCmdCH) + set.cmd_chEnable = ::atoi(_idCmdCH->getValue().c_str()); + if (_idSetCH) + set.TSetCH = ::atof(_idSetCH->getValue().c_str()); + } + + ~BoilerControl() + { + } + }; + +} + +void *getAPI_SmartBoiler(String subtype, String param) +{ + if (subtype == F("BoilerControl")) + { + return new _Boiler::BoilerControl(param); + } + else if (subtype == F("DHWControl")) + { + return new _Boiler::DHWControl(param); + } + else + { + return nullptr; + } +} diff --git a/src/modules/exec/SmartBoiler/modinfo.json b/src/modules/exec/SmartBoiler/modinfo.json new file mode 100644 index 00000000..9be13ca2 --- /dev/null +++ b/src/modules/exec/SmartBoiler/modinfo.json @@ -0,0 +1,136 @@ +{ + "menuSection": "executive_devices", + "configItem": [ + { + "global": 0, + "name": "BoilerControl", + "type": "Reading", + "subtype": "BoilerControl", + "id": "boiler", + "widget": "anydataDef", + "page": "Boiler", + "descr": "Котёл", + "int": 60, + "value": "...", + "LogLevel": 0, + "telegram": 1, + "idPID":"PID", + "idTboiler": "Tboiler", + "idTret": "Tret", + "idToutside": "Toutside", + "idStateCH":"StateCH", + "idStateFlame":"StateFlame", + "idModLevel":"ModLevel", + "idCmdCH":"CmdCH", + "idCmdDHW":"CmdDHW", + "idSetCH":"SetCH", + "idCtrlType":"CtrlType", + "rele1_Pwr": 1, + "rele2_Pwr": 2, + "rele3_Pwr": 4, + "changeRele":0, + "Pump": 0, + "minCH": 35, + "maxCH": 85, + "gistCH": 5, + "antiFreez":10 + }, + { + "global": 0, + "name": "DHWControl", + "type": "Reading", + "subtype": "DHWControl", + "id": "dhw", + "widget": "anydataDef", + "page": "Boiler", + "descr": "Котёл", + "int": 60, + "value": "...", + "idTdhw": "TDhw", + "idReleDhw": "ReleDhw", + "idCmdDHW":"CmdDHW", + "idStateDHW":"StateDHW", + "idSetDHW":"SetDHW", + "minDhw": 20, + "maxDhw": 60, + "gistDhw": 2 + } + ], + "about": { + "authorName": "Mikhail Bubnov", + "authorContact": "https://t.me/Mit4bmw", + "authorGit": "https://github.com/Mit4el", + "specialThanks": "", + "moduleName": "SmartBoiler", + "moduleVersion": "0.1", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "subTypes": [ + "BoilerControl", + "OpenThermSlave" + ], + "title": "SmartBoiler", + "moduleDesc": "Модуль для автоматизации электрического котла. Мозги котла с внешним протоколом opentherm", + "propInfo": { + "int": "Интервал отправки данных в MQTT и web интерфейс", + "telegram": "1- Будет отправлять в телеграмм оповещения при ошибках котла и пропаже сигнала от котла, остальные необходимо реализовывать через сценарий", + "MemberID": "SlaveMemberIDcode - код производителя котла, кем притворится котёл;) Менять в большинстве случаев не надо", + "idPID":"ID модуля ПИД регулятора, для расчета модуляции и включения тэнов в зависимости от температуры теплоносителя, в модуле TCHSet будет уставка СО, создать TCHSet и указать его в модуле ПИД", + "idTboiler": "ID датчика температуры подачи котла", + "idTret": "ID датчика температуры обратки котла", + "idToutside": "ID датчика уличной температуры", + "rele1_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele1", + "rele2_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele2, если нет, то 0 (ноль)", + "rele3_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele3, если нет, то 0 (ноль)", + "Pupm": "1-есть реле насоса (ID реле должно называться relePump), 0-нет реле насоса, насос управляется котлом без нас", + "minCH": "Граница установки температуры СО", + "maxCH": "Граница установки температуры СО", + "gistCH": "Гистерезис СО - нагрев СО включится если температура теплоносителя ниже уставки на указанные градусы (CHSet = 45гр, gistCH = 5гр, нагрев включится когда idTboiler = 40гр)", + "idTdhw": "ID датчика температуры ГВС, например в датчик в БКН", + "idReleDhw":"ID реле трехходового крана ГВС", + "gistDhw": "Гистерезис ГВС - нагрев ГВС включится если температура воды ниже уставки на указанные градусы", + "minDhw": "Граница установки температуры ГВС", + "maxDhw": "Граница установки температуры ГВС", + "changeRele":"Будет менять каждый раз при включении тэн 1->2->3->1...", + "antiFreez":"Режим анти-замерзания, Указывается температура, если опустится ниже указанной, то включится нарев один тэн и нагреет на +5гр от указанной" + }, + "funcInfo": [ + { + "name": "CHSet", + "descr": "Установить целевую температуру СО", + "params": [ + "тепмература СО (подачи) - bolier.CHSet(60)" + ] + }, + { + "name": "CHEnable", + "descr": "включить / выключить отопление", + "params": [ + "bolier.CHEnable(1) - вкл, bolier.CHEnable(0) - выкл, " + ] + }, + { + "name": "SetDHW", + "descr": "Установить целевую температуру ГВС", + "params": [ + "тепмература ГВС - dhw.SetDHW(40)" + ] + }, + { + "name": "DHWEnable", + "descr": "включить / выключить ГВС", + "params": [ + "dhw.DHWEnable(1) - вкл, dhw.DHWEnable(0) - выкл, " + ] + } + ] + }, + "defActive": true, + "usedLibs": { + "esp32_4mb3f": [], + "esp32*": [], + "esp82*": [] + } +} \ No newline at end of file diff --git a/src/modules/exec/SmartBoiler/readme.txt b/src/modules/exec/SmartBoiler/readme.txt new file mode 100644 index 00000000..3113d406 --- /dev/null +++ b/src/modules/exec/SmartBoiler/readme.txt @@ -0,0 +1,71 @@ +Модуль для автоматизации электрического котла. +(Описание модуля SmartBoiler/modinfo.json) + +0 TODO Сделать чтобы при связи моих модулей промежуточные итемы можно было не создавать, + но если их создать там отобразятся актуальные значения + +1 Управления котлом с 1-3 тэнами +1.1 указывается количество тэнов от 1 до 3х +1.2 задается мощность каждого тэна, должна быть по возрастающей +1.3 задание гистерезиса включения тэнов (ограничиваем в том числе и с ПИД) +1.4 задание минимальной и максимальной температуры теплоносителя (минимальную автоматически не поддерживает пока, просто проверяет вхождение в диапазон) +1.5 задание минимальной и максимальной температуры ГВС (минимальную автоматически не поддерживает пока, просто проверяет вхождение в диапазон) +1.6 реализация тремя отдельными элементами: + BoilerControl - Основная логика котла, по сути раздутый термостат + DHWControl - Логика управления нагрева ГВС + OpenThermSlave - Обеспечения протоокла взаимодействи OpenTherm +1.7 Смена тэна периодически (по флагу из конфигурации) +1.8 Если отвалились датчика, котел не включится +1.9 TODO режим антизамерзания +1.998 TODO поддержание минимальной температуры СО и ГВС + + +2 Поддержание температуры теплоносителя +2.1 Теплоноситель нагреется до заданной целевой и поддерживает её по гистерезису (если упала на гист. то включит нагрев) +2.2 ПИД расчитывает только количество тэнов для включения (0-30% = первый тэн, 30-60% = 1 и 2 тэны, 60-100% все тэны) +2.3 в зависимости от включенных тэнов и их мощности показывается процент модуляции. +2.4 Если не добавлен модуль ПИД, то включает все тэны +2.5 Если указан всего один тэн, то модуль ПИД не нужно создавать +2.998 TODO возможно использовать ПИД для периодического включения одного тэна +2.999 TODO сделать больше ступеней включения с различными комбинациями тэнов + +3 Управление из IoTManager +3.1 Возможность управления по комнатному термостату в том числе с другой ESP +3.2 Управление модулем из сценария +3.3 есть проверка ошибок датчиков (если отвалились датчика, котел не включится) +3.4 Отправка состояния в телеграмм +3.998 3.4 TODO Автоматическая отправка состояния в модули для отображения (имена модулей в логах "new") +3.999 другой функционал IoTManager ... + +4 Возможность управления циркуляционным насосом +4.1 насос включается всегда, если включено хотя бы один из тэнов +4.2 если включено по ГВС, насос отключается сразу как ГВС нагрелся до целевой +4.3 если включено по СО, насос отключается когда теплоноситель остынет до минимальной температуры СО +4.999 TODO Сделать управление выбегом насоса + +5 Возможность управления ГВС при наличии 3-х ходового крана с БКН (бойлер косвенного нагрева) +5.1 ГВС работает только при указании реле 3-х ходового крана (а иначе как?) +5.2 ГВС имеет приоритет над СО +5.3 при нагреве ГВС котел включается на полную мощность (все тэны) +5.4 ГВС нагреет до заданной целевой и поддерживает её по гистерезису (если упала на гист. то включит ГВС) +5.5 Температура подачи котла при этом не должны превысить минимальную температуру СО (что бы не перегревал тенплоноситель, пока греется БКН) + +6 Возможность управления по OpenTherm +6.1 в схему платы управления необходимо добавить часть OpenThermSlave и 24В +6.2 возможность управления любым OpenTherm адаптером/термостатом +6.3 задание целевой температуры теплоносителя +6.4 задание целевой температуры ГВС +6.5 команды включения СО и ГВС +6.6 отправка статуса Управляющему устройству OpenTherm +6.999 TODO Явного приоритета OpenTherm над другим управлением нет, а надо сделать. Сделать настройку "OpenTherm главне сценария" (сейчас команды выполняются ото всех по мере поступления) + +7. Название модулей в которые автоматически отправится информации при их наличии (простто добавить в конфигурацию с указанным именем) + controlType - Тип управления тэнами: 0 - модуляция, 1- вкл/выкл + CHEnable - Состояние включения СО (не нагрев, а включение режима) 0 - выкл, 1- вкл + isFlame - Состояние нагрева/горелки (включенных тэнов) 0 - выкл, 1- вкл + RelModLevel - Уровень модуляции, в процентах в зависимости мощности включенных тэнов от их общего количества + TDHWSet - Установленная в котле целевая температура ГВС (из Сценария или OpenTherm) + TCHSet - Установленная в котле целевая температура СО (из Сценария или OpenTherm) + DHWEnable Состояние включения ГВС (не нагрев, а включение режима) 0 - выкл, 1- вкл + boilerslave - Состояние подключения к Управляющему устройству OpenTherm, значком ❌ ✅ + status - Состояние подключения к Управляющему устройству OpenTherm, строкой: "не подключен" / "подключен" diff --git a/src/modules/exec/SmartBoiler/smartBoiler.json b/src/modules/exec/SmartBoiler/smartBoiler.json new file mode 100644 index 00000000..7af83ffe --- /dev/null +++ b/src/modules/exec/SmartBoiler/smartBoiler.json @@ -0,0 +1,337 @@ +{ + "mark": "iotm", + "config": [ + { + "global": 0, + "type": "Writing", + "subtype": "TelegramLT", + "id": "tg", + "widget": "", + "page": "", + "descr": "", + "token": "", + "chatID": "" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "Tboiler", + "needSave": 0, + "widget": "inputDgt", + "page": "Котёл", + "descr": "Датчик котла", + "int": "0", + "val": "20", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "SetCH", + "needSave": 0, + "widget": "inputDgt", + "page": "Котёл", + "descr": "Уставка СО", + "int": "0", + "val": "60", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "CtrlType", + "needSave": 0, + "widget": "anydataDef", + "page": "Состояние", + "descr": " Тип управления", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "CmdCH", + "needSave": 0, + "widget": "toggle", + "page": "Котёл", + "descr": " ВКЛ СО", + "int": "0", + "val": "0" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "StateCH", + "needSave": 0, + "widget": "anydataDef", + "page": "Состояние", + "descr": "Состояние СО", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "StateFlame", + "needSave": 0, + "widget": "anydataDef", + "page": "Состояние", + "descr": "Состояние Нагрева", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "rele1", + "needSave": 0, + "widget": "toggle", + "page": "Котёл", + "descr": "rele1", + "int": "0", + "val": "0" + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "rele2", + "needSave": 0, + "widget": "toggle", + "page": "Котёл", + "descr": "rele2", + "int": "0", + "val": "0" + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "rele3", + "needSave": 0, + "widget": "toggle", + "page": "Котёл", + "descr": "rele3", + "int": "0", + "val": "0" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "ModLevel", + "needSave": 0, + "widget": "anydataDef", + "page": "Состояние", + "descr": "Модуляция", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "relePump", + "needSave": 0, + "widget": "toggle", + "page": "Котёл", + "descr": "Реле цирк.насоса", + "int": "0", + "val": "0" + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "ReleDhw", + "needSave": 0, + "widget": "toggle", + "page": "ГВС", + "descr": "3-хходовой", + "int": "0", + "val": "0" + }, + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "CmdDHW", + "needSave": 0, + "widget": "toggle", + "page": "ГВС", + "descr": " ВКЛ ГВС", + "int": "0", + "val": "0" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "TDhw", + "needSave": 0, + "widget": "inputDgt", + "page": "ГВС", + "descr": "Датчик БКН", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "SetDHW", + "needSave": 0, + "widget": "inputDgt", + "page": "ГВС", + "descr": "Уставка ГВС", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "StateDHW", + "needSave": 0, + "widget": "anydataDef", + "page": "Состояние", + "descr": "Состояние ГВС", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "DHWControl", + "id": "dhw96", + "widget": "nil", + "page": "Boiler", + "descr": "Котёл", + "int": 60, + "value": "...", + "idTDhw": "TDhw", + "idReleDhw": "ReleDhw", + "idCmdDHW": "CmdDHW", + "idStateDHW": "StateDHW", + "idSetDHW": "SetDHW", + "minDhw": 20, + "maxDhw": 60, + "gistDhw": 2 + }, + { + "global": 0, + "needSave": 0, + "type": "Writing", + "subtype": "ThermostatPID", + "id": "PID", + "widget": "anydataHum", + "page": "Котёл", + "descr": "термостат ПИД", + "int": "10", + "round": 1, + "map": "0,100,0,100", + "set_id": "SetCH", + "term_id": "Tboiler", + "term_rezerv_id": "", + "rele": "", + "KP": 5, + "KI": 50, + "KD": 1 + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "status", + "needSave": 0, + "widget": "anydataDef", + "page": "Состояние", + "descr": " Состояние", + "int": "0", + "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "BoilerControl", + "id": "boiler81", + "widget": "nil", + "page": "Boiler", + "descr": "Котёл", + "int": 60, + "value": "...", + "LogLevel": 0, + "telegram": 1, + "idPID": "PID", + "idTboiler": "Tboiler", + "idTret": "Tret", + "idToutside": "Toutside", + "idStateCH": "StateCH", + "idStateFlame": "StateFlame", + "idModLevel": "ModLevel", + "idCmdCH": "CmdCH", + "idCmdDHW": "CmdDHW", + "idSetCH": "SetCH", + "idCtrlType": "CtrlType", + "rele1_Pwr": 1, + "rele2_Pwr": 2, + "rele3_Pwr": 4, + "changeRele": 0, + "Pump": 0, + "minCH": 35, + "maxCH": 85, + "gistCH": 5, + "antiFreez": 10 + } + ] +} + +scenario=>if onStart then + { + tg.sendMsg("SmartBoiler http://" + getIP()); + } From b4919bddd68312fd0bd6bba73bcd991b4361451d Mon Sep 17 00:00:00 2001 From: Mit4el Date: Mon, 26 Feb 2024 23:29:02 +0300 Subject: [PATCH 02/22] =?UTF-8?q?=D0=9C=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20Be?= =?UTF-8?q?nchmark?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/ESPConfiguration.h | 1 + include/Global.h | 3 + include/classes/IoTBench.h | 30 + include/classes/IoTItem.h | 6 + src/ESPConfiguration.cpp | 7 +- src/Global.cpp | 2 + src/Main.cpp | 24 +- src/modules/exec/OpenThermSlave/OpenTherm.cpp | 812 ------------------ src/modules/exec/OpenThermSlave/OpenTherm.h | 208 ----- .../exec/OpenThermSlave/OpenThermSlave.cpp | 589 ------------- src/modules/exec/OpenThermSlave/modinfo.json | 111 --- src/modules/virtual/Benchmark/Benchmark.cpp | 165 ++++ src/modules/virtual/Benchmark/modinfo.json | 69 ++ 13 files changed, 297 insertions(+), 1730 deletions(-) create mode 100644 include/classes/IoTBench.h delete mode 100644 src/modules/exec/OpenThermSlave/OpenTherm.cpp delete mode 100644 src/modules/exec/OpenThermSlave/OpenTherm.h delete mode 100644 src/modules/exec/OpenThermSlave/OpenThermSlave.cpp delete mode 100644 src/modules/exec/OpenThermSlave/modinfo.json create mode 100644 src/modules/virtual/Benchmark/Benchmark.cpp create mode 100644 src/modules/virtual/Benchmark/modinfo.json diff --git a/include/ESPConfiguration.h b/include/ESPConfiguration.h index 1e185219..7832008f 100644 --- a/include/ESPConfiguration.h +++ b/include/ESPConfiguration.h @@ -7,3 +7,4 @@ extern std::list IoTItems; // вектор ссылок базово extern void configure(String path); void clearConfigure(); extern IoTItem* myIoTItem; +extern IoTBench* myIoTBernch; diff --git a/include/Global.h b/include/Global.h index 4922d660..ac94ead5 100644 --- a/include/Global.h +++ b/include/Global.h @@ -50,6 +50,7 @@ #include "utils/StringUtils.h" #include "PeriodicTasks.h" #include "classes/IoTGpio.h" +#include "classes/IoTBench.h" /********************************************************************************************************************* *****************************************глобальные объекты классов*************************************************** @@ -58,6 +59,8 @@ extern IoTGpio IoTgpio; extern IoTItem* rtcItem; //extern IoTItem* camItem; extern IoTItem* tlgrmItem; +extern IoTBench* benchLoadItem; +extern IoTBench* benchTaskItem; extern TickerScheduler ts; extern WiFiClient espClient; diff --git a/include/classes/IoTBench.h b/include/classes/IoTBench.h new file mode 100644 index 00000000..d8743d72 --- /dev/null +++ b/include/classes/IoTBench.h @@ -0,0 +1,30 @@ +#pragma once +#include +#include "Global.h" +#include "classes/IoTItem.h" +#include + +struct ItemBench +{ + uint32_t sumloopTime = 0; + uint32_t loopTime = 0; + uint32_t loopTimeMax_p = 0; + uint32_t loopTimeMax_glob = 0; + uint32_t count = 0; +}; + + + +class IoTBench : public IoTItem +{ +public: + IoTBench(const String ¶meters); + ~IoTBench(); + + virtual void preLoadFunction(); + virtual void postLoadFunction(); + virtual void preTaskFunction(const String &id); + virtual void postTaskFunction(const String &id); +protected: +std::map banchItems; +}; \ No newline at end of file diff --git a/include/classes/IoTItem.h b/include/classes/IoTItem.h index 8eab3261..cfdb33e2 100644 --- a/include/classes/IoTItem.h +++ b/include/classes/IoTItem.h @@ -1,5 +1,8 @@ #pragma once #include "classes/IoTGpio.h" +//#include "classes/IoTBench.h" + +class IoTBench; struct IoTValue { float valD = 0; @@ -53,6 +56,9 @@ class IoTItem { virtual IoTItem* getRtcDriver(); //virtual IoTItem* getCAMDriver(); virtual IoTItem* getTlgrmDriver(); + //virtual IoTBench* getBenchmark(); + virtual IoTBench*getBenchmarkTask(); + virtual IoTBench*getBenchmarkLoad(); virtual unsigned long getRtcUnixTime(); // делаем доступным модулям отправку сообщений в телеграм diff --git a/src/ESPConfiguration.cpp b/src/ESPConfiguration.cpp index d0ee0bc8..fa15b2a1 100644 --- a/src/ESPConfiguration.cpp +++ b/src/ESPConfiguration.cpp @@ -33,6 +33,9 @@ void configure(String path) { if (driver = myIoTItem->getRtcDriver()) rtcItem = (IoTItem*)driver; // пробуем спросить драйвер CAM //if (driver = myIoTItem->getCAMDriver()) camItem = (IoTItem*)driver; + // пробуем спросить драйвер Benchmark + if (driver = myIoTItem->getBenchmarkTask()) benchTaskItem = ((IoTBench*)driver); + if (driver = myIoTItem->getBenchmarkLoad()) benchLoadItem = ((IoTBench*)driver); // пробуем спросить драйвер Telegram_v2 if (driver = myIoTItem->getTlgrmDriver()) tlgrmItem = (IoTItem*)driver; IoTItems.push_back(myIoTItem); @@ -48,7 +51,7 @@ void clearConfigure() { Serial.printf("Start clearing config\n"); rtcItem = nullptr; //camItem = nullptr; - tlgrmItem = nullptr; + tlgrmItem = nullptr; IoTgpio.clearDrivers(); for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { @@ -58,4 +61,6 @@ void clearConfigure() { IoTItems.clear(); valuesFlashJson.clear(); + benchTaskItem = nullptr; + benchLoadItem = nullptr; } \ No newline at end of file diff --git a/src/Global.cpp b/src/Global.cpp index 1130691c..7f6fe4d1 100644 --- a/src/Global.cpp +++ b/src/Global.cpp @@ -33,6 +33,8 @@ IoTGpio IoTgpio(0); IoTItem* rtcItem = nullptr; //IoTItem* camItem = nullptr; IoTItem* tlgrmItem = nullptr; +IoTBench* benchTaskItem = nullptr; +IoTBench* benchLoadItem = nullptr; String settingsFlashJson = "{}"; // переменная в которой хранятся все настройки, находится в оперативной памяти и синхронизированна с flash памятью String valuesFlashJson = "{}"; // переменная в которой хранятся все значения элементов, которые необходимо сохранить на flash. Находится в оперативной памяти и синхронизированна с flash памятью String errorsHeapJson = "{}"; // переменная в которой хранятся все ошибки, находится в оперативной памяти только diff --git a/src/Main.cpp b/src/Main.cpp index 75341d64..6e076812 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -16,8 +16,9 @@ String volStrForSave = ""; void elementsLoop() { // передаем управление каждому элементу конфигурации для выполнения своих функций for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { + if (benchTaskItem) benchTaskItem->preTaskFunction((*it)->getID()); (*it)->loop(); - + if (benchTaskItem) benchTaskItem->postTaskFunction((*it)->getID()); // if ((*it)->iAmDead) { if (!((*it)->iAmLocal) && (*it)->getIntFromNet() == -1) { delete *it; @@ -150,13 +151,13 @@ void setup() { iotScen.loadScenario("/scenario.txt"); // создаем событие завершения инициализации основных моментов для возможности выполнения блока кода при загрузке createItemFromNet("onInit", "1", 1); - elementsLoop(); +// elementsLoop(); //Для работы MQTT Брокера перенес ниже, иначе брокер падает если вызван до routerConnect(); stopErrorMarker(SETUPSCEN_ERRORMARKER); initErrorMarker(SETUPINET_ERRORMARKER); - // подключаемся к роутеру +// подключаемся к роутеру routerConnect(); // инициализация асинхронного веб сервера и веб сокетов @@ -179,6 +180,7 @@ void setup() { initErrorMarker(SETUPLAST_ERRORMARKER); + elementsLoop(); // NTP ntpInit(); @@ -224,31 +226,35 @@ void loop() { #ifdef LOOP_DEBUG unsigned long st = millis(); #endif - + if (benchLoadItem) benchLoadItem->preLoadFunction(); + if (benchTaskItem) benchTaskItem->preTaskFunction("TickerScheduler"); initErrorMarker(TICKER_ERRORMARKER); ts.update(); stopErrorMarker(TICKER_ERRORMARKER); - + if (benchTaskItem) benchTaskItem->postTaskFunction("TickerScheduler"); + if (benchTaskItem) benchTaskItem->preTaskFunction("webServer"); #ifdef STANDARD_WEB_SERVER initErrorMarker(HTTP_ERRORMARKER); HTTP.handleClient(); stopErrorMarker(HTTP_ERRORMARKER); #endif - + if (benchTaskItem) benchTaskItem->postTaskFunction("webServer"); + if (benchTaskItem) benchTaskItem->preTaskFunction("webSocket"); #ifdef STANDARD_WEB_SOCKETS initErrorMarker(SOCKETS_ERRORMARKER); standWebSocket.loop(); stopErrorMarker(SOCKETS_ERRORMARKER); #endif - + if (benchTaskItem) benchTaskItem->postTaskFunction("webSocket"); + if (benchTaskItem) benchTaskItem->preTaskFunction("mqtt"); initErrorMarker(MQTT_ERRORMARKER); mqttLoop(); stopErrorMarker(MQTT_ERRORMARKER); - + if (benchTaskItem) benchTaskItem->postTaskFunction("mqtt"); initErrorMarker(MODULES_ERRORMARKER); elementsLoop(); stopErrorMarker(MODULES_ERRORMARKER); - + if (benchLoadItem) benchLoadItem->postLoadFunction(); // #ifdef LOOP_DEBUG // loopPeriod = millis() - st; // if (loopPeriod > 2) Serial.println(loopPeriod); diff --git a/src/modules/exec/OpenThermSlave/OpenTherm.cpp b/src/modules/exec/OpenThermSlave/OpenTherm.cpp deleted file mode 100644 index 8f6f7467..00000000 --- a/src/modules/exec/OpenThermSlave/OpenTherm.cpp +++ /dev/null @@ -1,812 +0,0 @@ -/* -OpenTherm.cpp - OpenTherm Communication Library For Arduino, ESP8266 -Copyright 2018, Ihor Melnyk -*/ - -#include "OpenTherm.h" - -OpenTherm::OpenTherm(int inPin, int outPin, bool isSlave) : status(OpenThermStatus::NOT_INITIALIZED), - inPin(inPin), - outPin(outPin), - isSlave(isSlave), - response(0), - responseStatus(OpenThermResponseStatus::NONE), - responseTimestamp(0), - handleInterruptCallback(NULL), - processResponseCallback(NULL) -{ - imitFlag = false; -} - -void OpenTherm::begin(void (*handleInterruptCallback)(void), void (*processResponseCallback)(unsigned long, OpenThermResponseStatus)) -{ - pinMode(inPin, INPUT); - pinMode(outPin, OUTPUT); - if (handleInterruptCallback != NULL) - { - this->handleInterruptCallback = handleInterruptCallback; - attachInterrupt(digitalPinToInterrupt(inPin), handleInterruptCallback, CHANGE); - } - activateBoiler(); - status = OpenThermStatus::READY; - this->processResponseCallback = processResponseCallback; -} - -void OpenTherm::begin(void (*handleInterruptCallback)(void)) -{ - begin(handleInterruptCallback, NULL); -} - -bool IRAM_ATTR OpenTherm::isReady() -{ - return status == OpenThermStatus::READY; -} - -int IRAM_ATTR OpenTherm::readState() -{ - return digitalRead(inPin); -} - -void OpenTherm::setActiveState() -{ - digitalWrite(outPin, LOW); -} - -void OpenTherm::setIdleState() -{ - digitalWrite(outPin, HIGH); -} - -void OpenTherm::activateBoiler() -{ - setIdleState(); - delay(1000); -} - -void OpenTherm::sendBit(bool high) -{ - if (high) - setActiveState(); - else - setIdleState(); - delayMicroseconds(500); - if (high) - setIdleState(); - else - setActiveState(); - delayMicroseconds(500); -} - -bool OpenTherm::sendRequestAync(unsigned long request) -{ - // Serial.println("Request: " + String(request, HEX)); - noInterrupts(); - const bool ready = isReady(); - interrupts(); - - if (!ready) - return false; - - status = OpenThermStatus::REQUEST_SENDING; - response = 0; - responseStatus = OpenThermResponseStatus::NONE; -// Prevent switching to other tasks as there is a delay within sendBit -#ifdef ESP32 -// vTaskSuspendAll(); -#endif - sendBit(HIGH); // start bit - for (int i = 31; i >= 0; i--) - { - sendBit(bitRead(request, i)); - } - sendBit(HIGH); // stop bit - setIdleState(); -#ifdef ESP32 -// xTaskResumeAll(); -#endif - status = OpenThermStatus::RESPONSE_WAITING; - responseTimestamp = micros(); - if (imitFlag) - ImitationResponse(request); - return true; -} - -unsigned long OpenTherm::sendRequest(unsigned long request) -{ - if (!sendRequestAync(request)) - return 0; - while (!isReady()) - { - process(); - yield(); - } - return response; -} - -bool OpenTherm::sendResponse(unsigned long request) -{ - status = OpenThermStatus::REQUEST_SENDING; - response = 0; - responseStatus = OpenThermResponseStatus::NONE; - // Prevent switching to other tasks as there is a delay within sendBit -#ifdef ESP32 -// vTaskSuspendAll(); -#endif - sendBit(HIGH); // start bit - for (int i = 31; i >= 0; i--) - { - sendBit(bitRead(request, i)); - } - sendBit(HIGH); // stop bit - setIdleState(); -#ifdef ESP32 -// xTaskResumeAll(); -#endif - status = OpenThermStatus::READY; - return true; -} - -unsigned long OpenTherm::getLastResponse() -{ - return response; -} - -OpenThermResponseStatus OpenTherm::getLastResponseStatus() -{ - return responseStatus; -} - -void IRAM_ATTR OpenTherm::handleInterrupt() -{ - if (isReady()) - { - if (isSlave && readState() == HIGH) - { - status = OpenThermStatus::RESPONSE_WAITING; - } - else - { - return; - } - } - - unsigned long newTs = micros(); - if (status == OpenThermStatus::RESPONSE_WAITING) - { - if (readState() == HIGH) - { - status = OpenThermStatus::RESPONSE_START_BIT; - responseTimestamp = newTs; - } - else - { - // Error start bit / Ошибка стартового бита - status = OpenThermStatus::RESPONSE_INVALID; - responseTimestamp = newTs; - } - } - else if (status == OpenThermStatus::RESPONSE_START_BIT) - { - if ((newTs - responseTimestamp < 750) && readState() == LOW) - { - status = OpenThermStatus::RESPONSE_RECEIVING; - responseTimestamp = newTs; - responseBitIndex = 0; - } - else - { - // Error Start_bit LOW 750mks / Ошибка стартового бита по тылу (нет LOW через 750мкс) - status = OpenThermStatus::RESPONSE_INVALID; - responseTimestamp = newTs; - } - } - else if (status == OpenThermStatus::RESPONSE_RECEIVING) - { - // unsigned long bitDuration = newTs - responseTimestamp; - // В новой спецификации стоповый бит не обязателен. Если не дождались, всё равно попробуем разобрать - if ((newTs - responseTimestamp) > 750 && (newTs - responseTimestamp) < 1300) - { - if (responseBitIndex < 32) - { - response = (response << 1) | !readState(); - responseTimestamp = newTs; - responseBitIndex++; - } - else - { // stop bit - status = OpenThermStatus::RESPONSE_READY; - responseTimestamp = newTs; - } - } - } -} - -void OpenTherm::process() -{ - noInterrupts(); - OpenThermStatus st = status; - unsigned long ts = responseTimestamp; - interrupts(); - - if (st == OpenThermStatus::READY) - return; - unsigned long newTs = micros(); - if (st != OpenThermStatus::NOT_INITIALIZED && st != OpenThermStatus::DELAY && (newTs - ts) > 1000000) - { - status = OpenThermStatus::READY; - responseStatus = OpenThermResponseStatus::TIMEOUT; - if (processResponseCallback != NULL) - { - processResponseCallback(response, responseStatus); - } - } - else if (st == OpenThermStatus::RESPONSE_INVALID) - { - status = OpenThermStatus::DELAY; - responseStatus = OpenThermResponseStatus::INVALID; - if (processResponseCallback != NULL) - { - processResponseCallback(response, responseStatus); - } - } - else if (st == OpenThermStatus::RESPONSE_READY) - { - status = OpenThermStatus::DELAY; - responseStatus = (isSlave ? isValidRequest(response) : isValidResponse(response)) ? OpenThermResponseStatus::SUCCESS : OpenThermResponseStatus::INVALID; - // Error msgType (READ_ACK | WRITE_ACK) is Header - if (processResponseCallback != NULL) - { - processResponseCallback(response, responseStatus); - } - } - else if (st == OpenThermStatus::DELAY) - { - if ((newTs - ts) > 100000) - { - status = OpenThermStatus::READY; - } - } -} - -bool OpenTherm::parity(unsigned long frame) // odd parity -{ - byte p = 0; - while (frame > 0) - { - if (frame & 1) - p++; - frame = frame >> 1; - } - return (p & 1); -} - -OpenThermMessageType OpenTherm::getMessageType(unsigned long message) -{ - OpenThermMessageType msg_type = static_cast((message >> 28) & 7); - return msg_type; -} - -OpenThermMessageID OpenTherm::getDataID(unsigned long frame) -{ - return (OpenThermMessageID)((frame >> 16) & 0xFF); -} - -unsigned long OpenTherm::buildRequest(OpenThermMessageType type, OpenThermMessageID id, unsigned int data) -{ - unsigned long request = data; - if (type == OpenThermMessageType::WRITE_DATA) - { - request |= 1ul << 28; - } - request |= ((unsigned long)id) << 16; - if (parity(request)) - request |= (1ul << 31); - return request; -} -unsigned long OpenTherm::buildRequestID(OpenThermMessageType type, unsigned int id, unsigned int data) -{ - unsigned long request = data; - if (type == OpenThermMessageType::WRITE_DATA) - { - request |= 1ul << 28; - } - request |= ((unsigned long)id) << 16; - if (parity(request)) - request |= (1ul << 31); - return request; -} - -unsigned long OpenTherm::buildResponse(OpenThermMessageType type, OpenThermMessageID id, unsigned int data) -{ - unsigned long response = data; - response |= ((unsigned long)type) << 28; - response |= ((unsigned long)id) << 16; - if (parity(response)) - response |= (1ul << 31); - return response; -} - -bool OpenTherm::isValidResponse(unsigned long response) -{ - if (parity(response)) - return false; - byte msgType = (response << 1) >> 29; - return msgType == READ_ACK || msgType == WRITE_ACK; -} - -bool OpenTherm::isValidRequest(unsigned long request) -{ - if (parity(request)) - return false; - byte msgType = (request << 1) >> 29; - return msgType == READ_DATA || msgType == WRITE_DATA; -} - -void OpenTherm::end() -{ - if (this->handleInterruptCallback != NULL) - { - detachInterrupt(digitalPinToInterrupt(inPin)); - } -} - -const char *OpenTherm::statusToString(OpenThermResponseStatus status) -{ - switch (status) - { - case NONE: - return "NONE"; - case SUCCESS: - return "SUCCESS"; - case INVALID: - return "INVALID"; - case TIMEOUT: - return "TIMEOUT"; - default: - return "UNKNOWN"; - } -} - -const char *OpenTherm::messageTypeToString(OpenThermMessageType message_type) -{ - switch (message_type) - { - case READ_DATA: - return "READ_DATA"; - case WRITE_DATA: - return "WRITE_DATA"; - case INVALID_DATA: - return "INVALID_DATA"; - case RESERVED: - return "RESERVED"; - case READ_ACK: - return "READ_ACK"; - case WRITE_ACK: - return "WRITE_ACK"; - case DATA_INVALID: - return "DATA_INVALID"; - case UNKNOWN_DATA_ID: - return "UNKNOWN_DATA_ID"; - default: - return "UNKNOWN"; - } -} - -// building requests - -unsigned long OpenTherm::buildSetBoilerStatusRequest(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2, bool enableSummerMode, bool dhwBlock) -{ - unsigned int data = enableCentralHeating | (enableHotWater << 1) | (enableCooling << 2) | (enableOutsideTemperatureCompensation << 3) | (enableCentralHeating2 << 4) | (enableSummerMode << 5) | (dhwBlock << 6); - data <<= 8; - return buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Status, data); -} - -unsigned long OpenTherm::buildSetBoilerTemperatureRequest(float temperature) -{ - unsigned int data = temperatureToData(temperature); - return buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TSet, data); -} - -unsigned long OpenTherm::buildGetBoilerTemperatureRequest() -{ - return buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Tboiler, 0); -} - -// parsing responses -bool OpenTherm::isFault(unsigned long response) -{ - return response & 0x1; -} - -bool OpenTherm::isCentralHeatingActive(unsigned long response) -{ - return response & 0x2; -} - -bool OpenTherm::isHotWaterActive(unsigned long response) -{ - return response & 0x4; -} - -bool OpenTherm::isFlameOn(unsigned long response) -{ - return response & 0x8; -} - -bool OpenTherm::isCoolingActive(unsigned long response) -{ - return response & 0x10; -} - -bool OpenTherm::isDiagnostic(unsigned long response) -{ - return response & 0x40; -} - -uint16_t OpenTherm::getUInt(const unsigned long response) const -{ - const uint16_t u88 = response & 0xffff; - return u88; -} - -float OpenTherm::getFloat(const unsigned long response) const -{ - const uint16_t u88 = getUInt(response); - const float f = (u88 & 0x8000) ? -(0x10000L - u88) / 256.0f : u88 / 256.0f; - return f; -} - -unsigned int OpenTherm::temperatureToData(float temperature) -{ - if (temperature < 0) - temperature = 0; - if (temperature > 100) - temperature = 100; - unsigned int data = (unsigned int)(temperature * 256); - return data; -} - -// basic requests - -unsigned long OpenTherm::setBoilerStatus(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2, bool enableSummerMode, bool dhwBlock) -{ - return sendRequest(buildSetBoilerStatusRequest(enableCentralHeating, enableHotWater, enableCooling, enableOutsideTemperatureCompensation, enableCentralHeating2, enableSummerMode, dhwBlock)); -} - -bool OpenTherm::setBoilerTemperature(float temperature) -{ - unsigned long response = sendRequest(buildSetBoilerTemperatureRequest(temperature)); - return isValidResponse(response); -} - -float OpenTherm::getBoilerTemperature() -{ - unsigned long response = sendRequest(buildGetBoilerTemperatureRequest()); - return isValidResponse(response) ? getFloat(response) : 0; -} - -float OpenTherm::getReturnTemperature() -{ - unsigned long response = sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::Tret, 0)); - return isValidResponse(response) ? getFloat(response) : 0; -} - -bool OpenTherm::setDHWSetpoint(float temperature) -{ - unsigned int data = temperatureToData(temperature); - unsigned long response = sendRequest(buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TdhwSet, data)); - return isValidResponse(response); -} - -float OpenTherm::getDHWTemperature() -{ - unsigned long response = sendRequest(buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Tdhw, 0)); - return isValidResponse(response) ? getFloat(response) : 0; -} - -float OpenTherm::getModulation() -{ - unsigned long response = sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::RelModLevel, 0)); - return isValidResponse(response) ? getFloat(response) : 0; -} - -float OpenTherm::getPressure() -{ - unsigned long response = sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::CHPressure, 0)); - return isValidResponse(response) ? getFloat(response) : 0; -} - -unsigned char OpenTherm::getFault() -{ - return ((sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::ASFflags, 0)) >> 8) & 0xff); -} -int8_t flame_timer = 0; -void OpenTherm::ImitationResponse(unsigned long request) -{ - - // unsigned long response; - unsigned int data = getUInt(request); - OpenThermMessageType msgType; - byte ID; - OpenThermMessageID id = getDataID(request); - uint8_t flags; - - switch (id) - { - case OpenThermMessageID::Status: - // Статус котла получен - msgType = OpenThermMessageType::READ_ACK; - static int8_t flame = 0; - flame_timer++; - if (flame_timer > 10) - flame = 1; - if (flame_timer > 20) - { - flame_timer = 0; - flame = 0; - } - static int8_t fault = 0; - // fault = 1 - fault; - data = (bool)fault | (true << 1) | (true << 2) | ((bool)flame << 3) | (false << 4); - break; - case OpenThermMessageID::SConfigSMemberIDcode: - msgType = OpenThermMessageType::READ_ACK; - break; - case OpenThermMessageID::SlaveVersion: - msgType = OpenThermMessageType::READ_ACK; - break; - case OpenThermMessageID::MasterVersion: - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::RelModLevel: - static float RelModLevel = 10; - // RelModLevel = RelModLevel > 100 ? 10 : RelModLevel + 1; - if (flame_timer < 11) - { - RelModLevel = 0; - } - else - { - RelModLevel = RelModLevel == 0 ? 10 : RelModLevel + 1; - } - // data = RelModLevel; - data = temperatureToData(RelModLevel); - msgType = OpenThermMessageType::READ_ACK; - break; - case OpenThermMessageID::Tboiler: - // Получили температуру котла - static float Tboiler = 40; - Tboiler = Tboiler > 60 ? 40 : Tboiler + 1; - data = temperatureToData(Tboiler); - msgType = OpenThermMessageType::READ_ACK; - break; - case OpenThermMessageID::Tdhw: - // Получили температуру ГВС - static float Tdhw = 60; - Tdhw = Tdhw > 80 ? 60 : Tdhw + 1; - data = temperatureToData(Tdhw); - msgType = OpenThermMessageType::READ_ACK; - break; - case OpenThermMessageID::Toutside: - // Получили внешнюю температуру - static float Toutside = -10; - Toutside = Toutside > 10 ? -10 : Toutside + 1; - data = temperatureToData(Toutside); - msgType = OpenThermMessageType::READ_ACK; - break; - case OpenThermMessageID::ASFflags: - msgType = OpenThermMessageType::READ_ACK; - break; - - case OpenThermMessageID::TdhwSetUBTdhwSetLB: - msgType = OpenThermMessageType::READ_ACK; - break; - case OpenThermMessageID::MaxTSetUBMaxTSetLB: - msgType = OpenThermMessageType::READ_ACK; - break; - - case OpenThermMessageID::OEMDiagnosticCode: - msgType = OpenThermMessageType::READ_ACK; - break; - - case OpenThermMessageID::OpenThermVersionSlave: - msgType = OpenThermMessageType::READ_ACK; - break; - - case OpenThermMessageID::CHPressure: - msgType = OpenThermMessageType::READ_ACK; - break; - - break; - case OpenThermMessageID::DHWFlowRate: - msgType = OpenThermMessageType::READ_ACK; - break; - case OpenThermMessageID::DayTime: - if (getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::Date: - if (getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::Year: - if (getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - - case OpenThermMessageID::Tret: - // - msgType = OpenThermMessageType::READ_ACK; - break; - case OpenThermMessageID::Tstorage: - // - msgType = OpenThermMessageType::READ_ACK; - break; - case OpenThermMessageID::Tcollector: - // - msgType = OpenThermMessageType::READ_ACK; - break; - case OpenThermMessageID::TflowCH2: - // - msgType = OpenThermMessageType::READ_ACK; - - break; - case OpenThermMessageID::Tdhw2: - // - msgType = OpenThermMessageType::READ_ACK; - - break; - case OpenThermMessageID::Texhaust: - // - msgType = OpenThermMessageType::READ_ACK; - break; - case OpenThermMessageID::TheatExchanger: - // - msgType = OpenThermMessageType::READ_ACK; - break; - case OpenThermMessageID::BoilerFanSpeed: - // - msgType = OpenThermMessageType::READ_ACK; - break; - case OpenThermMessageID::ElectricBurnerFlame: - // - msgType = OpenThermMessageType::READ_ACK; - break; - case OpenThermMessageID::BurnerStarts: - // - if (getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::CHPumpStarts: - // - if (getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::DHWPumpValveStarts: - // - if (getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::DHWBurnerStarts: - // - if (getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::BurnerOperationHours: - // - if (getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::CHPumpOperationHours: - // - if (getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::DHWPumpValveOperationHours: - // - if (getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::DHWBurnerOperationHours: - // - if (getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::RBPflags: - // - // Pre-Defined Remote Boiler Parameters - // - msgType = OpenThermMessageType::READ_ACK; - break; - case OpenThermMessageID::TdhwSet: - // - if (getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::TSet: - // - if (getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::MaxTSet: - // - if (getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::Hcratio: - // - if (getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::TSP: - // - // Transparent Slave Parameters - // - msgType = OpenThermMessageType::READ_ACK; - break; - case OpenThermMessageID::FHBsize: - // - // Fault History Data - // - msgType = OpenThermMessageType::READ_ACK; - break; - case OpenThermMessageID::MaxCapacityMinModLevel: - // - // Boiler Sequencer Control - // - msgType = OpenThermMessageType::READ_ACK; - break; - case OpenThermMessageID::TrOverride: - // - // Remote override room setpoint - // - msgType = OpenThermMessageType::READ_ACK; - break; - case OpenThermMessageID::RemoteOverrideFunction: - msgType = OpenThermMessageType::READ_ACK; - break; - - default: - msgType = OpenThermMessageType::UNKNOWN_DATA_ID; - break; - } - response = buildResponse(msgType, id, data); - status = OpenThermStatus::RESPONSE_READY; - responseStatus = OpenThermResponseStatus::SUCCESS; - /* - if (processResponseCallback != NULL) - { - processResponseCallback(response, OpenThermResponseStatus::SUCCESS); - } - */ -} \ No newline at end of file diff --git a/src/modules/exec/OpenThermSlave/OpenTherm.h b/src/modules/exec/OpenThermSlave/OpenTherm.h deleted file mode 100644 index aeea5d1b..00000000 --- a/src/modules/exec/OpenThermSlave/OpenTherm.h +++ /dev/null @@ -1,208 +0,0 @@ -/* -OpenTherm.h - OpenTherm Library for the ESP8266/Arduino platform -https://github.com/ihormelnyk/OpenTherm -http://ihormelnyk.com/pages/OpenTherm -Licensed under MIT license -Copyright 2018, Ihor Melnyk - -Frame Structure: -P MGS-TYPE SPARE DATA-ID DATA-VALUE -0 000 0000 00000000 00000000 00000000 -*/ - -#ifndef OpenTherm_h -#define OpenTherm_h - -#include -#include - -enum OpenThermResponseStatus : uint8_t -{ - NONE, - SUCCESS, - INVALID, - TIMEOUT -}; - -enum OpenThermMessageType : uint8_t -{ - /* Master to Slave */ - READ_DATA = B000, - READ = READ_DATA, // for backwared compatibility - WRITE_DATA = B001, - WRITE = WRITE_DATA, // for backwared compatibility - INVALID_DATA = B010, - RESERVED = B011, - /* Slave to Master */ - READ_ACK = B100, - WRITE_ACK = B101, - DATA_INVALID = B110, - UNKNOWN_DATA_ID = B111 -}; - -typedef OpenThermMessageType OpenThermRequestType; // for backwared compatibility - -enum OpenThermMessageID : uint8_t -{ - Status, // flag8 / flag8 Master and Slave Status flags. - TSet, // f8.8 Control setpoint ie CH water temperature setpoint (°C) - MConfigMMemberIDcode, // flag8 / u8 Master Configuration Flags / Master MemberID Code - SConfigSMemberIDcode, // flag8 / u8 Slave Configuration Flags / Slave MemberID Code - Command, // u8 / u8 Remote Command - ASFflags, // / OEM-fault-code flag8 / u8 Application-specific fault flags and OEM fault code - RBPflags, // flag8 / flag8 Remote boiler parameter transfer-enable & read/write flags - CoolingControl, // f8.8 Cooling control signal (%) - TsetCH2, // f8.8 Control setpoint for 2e CH circuit (°C) - TrOverride, // f8.8 Remote override room setpoint - TSP, // u8 / u8 Number of Transparent-Slave-Parameters supported by slave - TSPindexTSPvalue, // u8 / u8 Index number / Value of referred-to transparent slave parameter. - FHBsize, // u8 / u8 Size of Fault-History-Buffer supported by slave - FHBindexFHBvalue, // u8 / u8 Index number / Value of referred-to fault-history buffer entry. - MaxRelModLevelSetting, // f8.8 Maximum relative modulation level setting (%) - MaxCapacityMinModLevel, // u8 / u8 Maximum boiler capacity (kW) / Minimum boiler modulation level(%) - TrSet, // f8.8 Room Setpoint (°C) - RelModLevel, // f8.8 Relative Modulation Level (%) - CHPressure, // f8.8 Water pressure in CH circuit (bar) - DHWFlowRate, // f8.8 Water flow rate in DHW circuit. (litres/minute) - DayTime, // special / u8 Day of Week and Time of Day - Date, // u8 / u8 Calendar date - Year, // u16 Calendar year - TrSetCH2, // f8.8 Room Setpoint for 2nd CH circuit (°C) - Tr, // f8.8 Room temperature (°C) - Tboiler, // f8.8 Boiler flow water temperature (°C) - Tdhw, // f8.8 DHW temperature (°C) - Toutside, // f8.8 Outside temperature (°C) - Tret, // f8.8 Return water temperature (°C) - Tstorage, // f8.8 Solar storage temperature (°C) - Tcollector, // f8.8 Solar collector temperature (°C) - TflowCH2, // f8.8 Flow water temperature CH2 circuit (°C) - Tdhw2, // f8.8 Domestic hot water temperature 2 (°C) - Texhaust, // s16 Boiler exhaust temperature (°C) - TheatExchanger, // f8.8 Boiler heat exchanger temperature (°C) - BoilerFanSpeed, // u16 Boiler fan speed Setpiont and actual value - ElectricBurnerFlame, // f8.8?? Electric current through burner flame (mюA) - TdhwSetUBTdhwSetLB = 48, // s8 / s8 DHW setpoint upper & lower bounds for adjustment (°C) - MaxTSetUBMaxTSetLB, // s8 / s8 Max CH water setpoint upper & lower bounds for adjustment (°C) - HcratioUBHcratioLB, // s8 / s8 OTC heat curve ratio upper & lower bounds for adjustment - TdhwSet = 56, // f8.8 DHW setpoint (°C) (Remote parameter 1) - MaxTSet, // f8.8 Max CH water setpoint (°C) (Remote parameters 2) - Hcratio, // f8.8 OTC heat curve ratio (°C) (Remote parameter 3) - RemoteOverrideFunction = 100, // flag8 / - Function of manual and program changes in master and remote room setpoint. - OEMDiagnosticCode = 115, // u16 OEM-specific diagnostic/service code - BurnerStarts, // u16 Number of starts burner - CHPumpStarts, // u16 Number of starts CH pump - DHWPumpValveStarts, // u16 Number of starts DHW pump/valve - DHWBurnerStarts, // u16 Number of starts burner during DHW mode - BurnerOperationHours, // u16 Number of hours that burner is in operation (i.e. flame on) - CHPumpOperationHours, // u16 Number of hours that CH pump has been running - DHWPumpValveOperationHours, // u16 Number of hours that DHW pump has been running or DHW valve has been opened - DHWBurnerOperationHours, // u16 Number of hours that burner is in operation during DHW mode - OpenThermVersionMaster, // f8.8 The implemented version of the OpenTherm Protocol Specification in the master. - OpenThermVersionSlave, // f8.8 The implemented version of the OpenTherm Protocol Specification in the slave. - MasterVersion, // u8 / u8 Master product version number and type - SlaveVersion, // u8 / u8 Slave product version number and type -}; - -enum OpenThermStatus : uint8_t -{ - NOT_INITIALIZED, - READY, - DELAY, - REQUEST_SENDING, - RESPONSE_WAITING, - RESPONSE_START_BIT, - RESPONSE_RECEIVING, - RESPONSE_READY, - RESPONSE_INVALID -}; - -class OpenTherm -{ -public: - OpenTherm(int inPin = 4, int outPin = 5, bool isSlave = false); - volatile OpenThermStatus status; - void begin(void (*handleInterruptCallback)(void)); - void begin(void (*handleInterruptCallback)(void), void (*processResponseCallback)(unsigned long, OpenThermResponseStatus)); - bool isReady(); - unsigned long sendRequest(unsigned long request); - bool sendResponse(unsigned long request); - bool sendRequestAync(unsigned long request); - unsigned long buildRequest(OpenThermMessageType type, OpenThermMessageID id, unsigned int data); - unsigned long buildRequestID(OpenThermMessageType type, unsigned int id, unsigned int data); - unsigned long buildResponse(OpenThermMessageType type, OpenThermMessageID id, unsigned int data); - unsigned long getLastResponse(); - OpenThermResponseStatus getLastResponseStatus(); - const char *statusToString(OpenThermResponseStatus status); - void handleInterrupt(); - void process(); - void end(); - - bool parity(unsigned long frame); - OpenThermMessageType getMessageType(unsigned long message); - OpenThermMessageID getDataID(unsigned long frame); - const char *messageTypeToString(OpenThermMessageType message_type); - bool isValidRequest(unsigned long request); - bool isValidResponse(unsigned long response); - - // requests - unsigned long buildSetBoilerStatusRequest(bool enableCentralHeating, bool enableHotWater = false, bool enableCooling = false, bool enableOutsideTemperatureCompensation = false, bool enableCentralHeating2 = false, bool enableSummerMode = false, bool dhwBlock = false); - unsigned long buildSetBoilerTemperatureRequest(float temperature); - unsigned long buildGetBoilerTemperatureRequest(); - - // responses - bool isFault(unsigned long response); - bool isCentralHeatingActive(unsigned long response); - bool isHotWaterActive(unsigned long response); - bool isFlameOn(unsigned long response); - bool isCoolingActive(unsigned long response); - bool isDiagnostic(unsigned long response); - uint16_t getUInt(const unsigned long response) const; - float getFloat(const unsigned long response) const; - unsigned int temperatureToData(float temperature); - - // basic requests - unsigned long setBoilerStatus(bool enableCentralHeating, bool enableHotWater = false, bool enableCooling = false, bool enableOutsideTemperatureCompensation = false, bool enableCentralHeating2 = false, bool enableSummerMode = false, bool dhwBlock = false); - bool setBoilerTemperature(float temperature); - float getBoilerTemperature(); - float getReturnTemperature(); - bool setDHWSetpoint(float temperature); - float getDHWTemperature(); - float getModulation(); - float getPressure(); - unsigned char getFault(); - - //Имитация ответов от котла, TRUE - идет имитация ответов котла, в котел так же шлется (лучше его отключить), FALSE - штатная работа - void imitation(bool fl) {imitFlag = fl;} - -private: - bool imitFlag; - void ImitationResponse(unsigned long request); - - const int inPin; - const int outPin; - const bool isSlave; - - volatile unsigned long response; - volatile OpenThermResponseStatus responseStatus; - volatile unsigned long responseTimestamp; - volatile byte responseBitIndex; - - int readState(); - void setActiveState(); - void setIdleState(); - void activateBoiler(); - - void sendBit(bool high); - void (*handleInterruptCallback)(); - void (*processResponseCallback)(unsigned long, OpenThermResponseStatus); -}; - -#ifndef ICACHE_RAM_ATTR -#define ICACHE_RAM_ATTR -#endif - -#ifndef IRAM_ATTR -#define IRAM_ATTR ICACHE_RAM_ATTR -#endif - -#endif // OpenTherm_h diff --git a/src/modules/exec/OpenThermSlave/OpenThermSlave.cpp b/src/modules/exec/OpenThermSlave/OpenThermSlave.cpp deleted file mode 100644 index 59357de9..00000000 --- a/src/modules/exec/OpenThermSlave/OpenThermSlave.cpp +++ /dev/null @@ -1,589 +0,0 @@ -#include "Global.h" -#include "classes/IoTItem.h" -#include -#include "OpenTherm.h" - -#define SLAVE true -#define TIMEOUT_TRESHOLD 5 - -namespace _OpenThermSlave -{ - OpenTherm *ot_driver = nullptr; - OpenTherm *instance_OTdriver(int _RX_pin, int _TX_pin) - { - if (!ot_driver) - { - ot_driver = new OpenTherm(_RX_pin, _TX_pin, SLAVE); - // ot_driver->begin(); - } - return ot_driver; - } - - // Обработчик прерываний от ОТ - void IRAM_ATTR handleInterruptSlave() - { - if (ot_driver != nullptr) - ot_driver->handleInterrupt(); - } - - // команды/установки от термостата - struct SetpointBoiler - { - uint8_t cmd_chEnable = 0; - uint8_t cmd_dhwEnable = 0; - float TSetCH = 0; - float TSetDhw = 0; - } set; - - struct failCode - { - bool service_required = 0; - bool lockout_reset = 0; - bool low_water_pressure = 0; - bool gas_fault = 0; - bool air_fault = 0; - bool water_overtemp = 0; - uint8_t fault_code = 0; - }; - - // текущее реальное состояние котла - struct StateBoiler - { - uint8_t stateCH = 0; - uint8_t stateDHW = 0; - uint8_t fl_flame = 0; - uint8_t fl_fail = 0; - failCode fCode; - float RelModLevel = 0; - float Tboiler = 0; - float Tret = 0; - float Tdhw = 0; - float Toutside = 0; - } state; - - // конфигурация котла - struct ConfigBoiler - { - bool dhw = false; // 1- есть реле(трехходовой) ГВС - bool ctrlType = false; // 0 - модуляция, 1- вкл/выкл - bool confDhw = true; // 1 - бак, 0 - проточная //TODO ПОКА НЕ ЗНАЮ ЧТО ДЕЛАТЬ - bool pumpControlMaster = false; // в протоколе ОТ: мастер управляет насосом ????????????????????? //TODO Команды кправления насосом от мастера не помню - - int minDhw; - int maxDhw; - int minCH; - int maxCH; - - } conf; - - // DynamicJsonDocument OpenThemData(JSON_BUFFER_SIZE / 2); - - IoTItem *tmp; - - IoTItem *_idTboiler = nullptr; - IoTItem *_idTret = nullptr; - IoTItem *_idToutside = nullptr; - IoTItem *_idStateCH = nullptr; - IoTItem *_idStateDHW = nullptr; - IoTItem *_idStateFlame = nullptr; - IoTItem *_idModLevel = nullptr; - IoTItem *_idTDhw = nullptr; - IoTItem *_idCmdCH = nullptr; - IoTItem *_idCmdDHW = nullptr; - IoTItem *_idSetCH = nullptr; - IoTItem *_idSetDHW = nullptr; - IoTItem *_idCtrlType = nullptr; - - unsigned long timeout_count = 0; - - uint8_t _debug = 0; - bool _telegram = false; - unsigned long ot_response = 0; - uint8_t SlaveMemberIDcode = 0; - - - void publishNew(String widget, String value) - { - tmp = findIoTItem(widget); - if (tmp) - { - tmp->setValue(value, true); - } - else - { - if (_debug > 0) - SerialPrint("new", "SmartBoiler", widget + " = " + value); - } - } - - void sendTelegramm(String msg) - { - if (_telegram == 1) - { - if (tlgrmItem) - tlgrmItem->sendTelegramMsg(false, msg); - } - } - - /* - * ========================================================================================= - * КЛАСС РАБОТЫ ПО ПРОТОКОЛУ OPENTHERM - * ========================================================================================= - */ - class OpenThermSlave : public IoTItem - { - private: - // unsigned long ts = 0; - - - - public: - OpenThermSlave(String parameters) : IoTItem(parameters) - { - int _RX_pin = 16; - int _TX_pin = 4; - SerialPrint("i", F("OpenThermSlave"), " START... "); - - jsonRead(parameters, "RX_pin", _RX_pin); - jsonRead(parameters, "TX_pin", _TX_pin); - jsonRead(parameters, "MemberID", (int &)SlaveMemberIDcode); - - jsonRead(parameters, "LogLevel", (int &)_debug); - jsonRead(parameters, "telegram", _telegram); - - String tmpID; - jsonRead(parameters, "idTboiler", tmpID); - _idTboiler = findIoTItem(tmpID); - jsonRead(parameters, "idTret", tmpID); - _idTret = findIoTItem(tmpID); - jsonRead(parameters, "idToutside", tmpID); - _idToutside = findIoTItem(tmpID); - jsonRead(parameters, "idStateCH", tmpID); - _idStateCH = findIoTItem(tmpID); - jsonRead(parameters, "idStateDHW", tmpID); - _idStateDHW = findIoTItem(tmpID); - jsonRead(parameters, "idStateFlame", tmpID); - _idStateFlame = findIoTItem(tmpID); - jsonRead(parameters, "idModLevel", tmpID); - _idModLevel = findIoTItem(tmpID); - jsonRead(parameters, "idTDhw", tmpID); - _idTDhw = findIoTItem(tmpID); - jsonRead(parameters, "idCmdCH", tmpID); - _idCmdCH = findIoTItem(tmpID); - jsonRead(parameters, "idCmdDHW", tmpID); - _idCmdDHW = findIoTItem(tmpID); - jsonRead(parameters, "idCtrlType", tmpID); - _idCtrlType = findIoTItem(tmpID); - - jsonRead(parameters, "idSetCH", tmpID); - _idSetCH = findIoTItem(tmpID); - jsonRead(parameters, "idSetDHW", tmpID); - _idSetDHW = findIoTItem(tmpID); - - jsonRead(parameters, "minCH", conf.minCH); - jsonRead(parameters, "maxCH", conf.maxCH); - jsonRead(parameters, "minDhw", conf.minDhw); - jsonRead(parameters, "maxDhw", conf.maxDhw); - - instance_OTdriver(_RX_pin, _TX_pin); - ot_driver->begin(handleInterruptSlave, processRequest); // responseCallback -// ot_boiler = this; - } - - void doByInterval() - { - } - - // Основной цикл программы - void loop() - { - ot_driver->process(); - IoTItem::loop(); - } - - // Комманды из сценария - IoTValue execute(String command, std::vector ¶m) - { - return {}; - } - - // Обработка управления и отправка статуса - static void processStatus(unsigned int &data) - { - - uint8_t statusRequest = data >> 8; // забрали старший байт с командами мастера - set.cmd_chEnable = statusRequest & 0x1; // забрали 0 бит из этого байта - включение СО (маска 01) - - set.cmd_dhwEnable = statusRequest & 0x2; // забрали 1 бит из этого байта - включение СО (маска 10) - IoTValue val; - val.valD = set.cmd_chEnable; - _idCmdCH->setValue(val, true); - val.valD = set.cmd_dhwEnable; - _idCmdDHW->setValue(val, true); - data &= 0xFF00; // старший бит не трогаем, а младший обнулили, что бы его заполнить состоянием котла и вернуть data термостату - - // if (_idFail) - // state.fl_fail = ::atof(_idFail->getValue().c_str()); - if (_idStateCH) - state.stateCH = ::atoi(_idStateCH->getValue().c_str()); - if (_idStateDHW) - state.stateDHW = ::atoi(_idStateDHW->getValue().c_str()); - if (_idStateFlame) - state.fl_flame = ::atoi(_idStateFlame->getValue().c_str()); - - if (state.fl_fail) - data |= 0x01; // fault indication - if (state.stateCH) - data |= 0x02; // CH active - if (state.stateDHW) - data |= 0x04; // DHW active - if (state.fl_flame) - data |= 0x08; // flame on - // data |= 0x10; //cooling active - // data |= 0x20; //CH2 active - // data |= 0x40; //diagnostic/service event - // data |= 0x80; //electricity production on - } - - // обработка сброса ошибок - static void processCommand(unsigned int &data) - { - uint8_t command = data >> 8; // забрали старший байт с командами мастера - if (command == 1) - { - state.fl_fail = 0; - data |= 128; // ответ 128-255: команда выполнена - } - } - - //=================================== Обработка входящих сообщение ОТ ====================================== - static void processRequest(unsigned long request, OpenThermResponseStatus status) - { - switch (status) - { - case OpenThermResponseStatus::NONE: - if (_debug > 0) - { - SerialPrint("E", "OpenThermSlave", "Error: OpenTherm не инициализирован"); - } - break; - case OpenThermResponseStatus::INVALID: - if (_debug > 0) - { - SerialPrint("E", "OpenThermSlave", "ID:" + String(ot_driver->getDataID(request)) + " / Error: Ошибка разбора команды: " + String(request, HEX)); - // build UNKNOWN-DATAID response - unsigned long response = ot_driver->buildResponse(OpenThermMessageType::DATA_INVALID, ot_driver->getDataID(request), 0); - - // send response - ot_driver->sendResponse(response); - } - break; - case OpenThermResponseStatus::TIMEOUT: - if (_debug > 0) - { - SerialPrint("E", "OpenThermSlave", " ID: " + String(ot_driver->getDataID(request)) + " / Error: Таймаут команд от управляющего устройства (термостата)"); - } - timeout_count++; - if (timeout_count > TIMEOUT_TRESHOLD) - { - publishNew("boilerslave", "❌"); - // publishNew("status", "не подключен"); - timeout_count = TIMEOUT_TRESHOLD; - sendTelegramm(("OpenTherm: потеря связи с управляющим устройством (термостатом) ❌")); - } - break; - case OpenThermResponseStatus::SUCCESS: - timeout_count = 0; - publishNew("boilerslave", "✅"); - // publishNew("status", "подключен"); - // sendTelegramm(("OpenTherm: котёл подключен ✅")); - // respondense_flag = true; - // ts_ = new_ts_; - HandleRequest(request); - break; - default: - break; - } - } - - // Парсинг запросов - static void HandleRequest(unsigned long request) - { - if (_idCtrlType) - conf.ctrlType = ::atoi(_idCtrlType->getValue().c_str()); - // unsigned long response; - unsigned int data = ot_driver->getUInt(request); - OpenThermMessageType msgType; - byte ID; - OpenThermMessageID id = ot_driver->getDataID(request); - uint8_t flags; - if (_debug > 2) - { - SerialPrint("i", "OpenThermSlave <-", String(millis()) + " ID: " + String(id) + " / requestHEX: " + String(request, HEX) + " / request: " + String(request)); - } - switch (id) - { - /*----------------------------Инициализация и конфигурация----------------------------*/ - case OpenThermMessageID::SConfigSMemberIDcode: // запрос Конфигурации Котла и SlaveMemberID - msgType = OpenThermMessageType::READ_ACK; - data = conf.dhw | (conf.ctrlType << 1) | (false << 2) | (conf.confDhw << 3) | (conf.pumpControlMaster << 4) | (false << 5); // 2-cooling, 5-CH2 - data <<= 8; - data |= SlaveMemberIDcode; - // data = (int)SlaveMemberIDcode; - break; - // case OpenThermMessageID::MConfigMMemberIDcode: // Получили Master Member ID - // msgType = OpenThermMessageType::WRITE_ACK; - // break; - // case OpenThermMessageID::SlaveVersion: // TODO вернуть версию модуля - // msgType = OpenThermMessageType::READ_ACK; - // data = (int)1; - // break; - // case OpenThermMessageID::MasterVersion: - // msgType = OpenThermMessageType::WRITE_ACK; - // break; - // case OpenThermMessageID::OpenThermVersionSlave: - // msgType = OpenThermMessageType::READ_ACK; - // break; - /*----------------------------Управление (уставки и команды)----------------------------*/ - case OpenThermMessageID::TdhwSetUBTdhwSetLB: // границы уставки ГВС - msgType = OpenThermMessageType::READ_ACK; - data |= (uint8_t)conf.minDhw; - data |= (uint8_t)conf.maxDhw << 8; - break; - case OpenThermMessageID::MaxTSetUBMaxTSetLB: // границы уставки СО - msgType = OpenThermMessageType::READ_ACK; - data |= (uint8_t)conf.minCH; - data |= (uint8_t)conf.maxCH << 8; - break; - case OpenThermMessageID::Command: // Сброс ошибок/сброс блокировки котла. Ответ: команды (не)выполнена - msgType = OpenThermMessageType::READ_ACK; - processCommand(data); - break; - case OpenThermMessageID::TdhwSet: // TODO Получили температуру ГВС - if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) - { - msgType = OpenThermMessageType::READ_ACK; - if (_idSetDHW) - set.TSetDhw = ::atof(_idSetDHW->getValue().c_str()); - data = ot_driver->temperatureToData(set.TSetDhw); - } - else - { - msgType = OpenThermMessageType::WRITE_ACK; - // processDHWSet(ot_driver->getFloat(data)); - set.TSetDhw = ot_driver->getFloat(data); - set.TSetDhw = constrain(set.TSetDhw, conf.minDhw, conf.maxDhw); - // publishNew("TDHWSet", String(set.TSetDhw)); - IoTValue val; - val.valD = set.TSetDhw; - _idSetDHW->setValue(val, true); - } - break; - case OpenThermMessageID::TSet: // TODO Получили температуру СО - if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) - { - msgType = OpenThermMessageType::READ_ACK; - if (_idSetCH) - set.TSetCH = ::atof(_idSetCH->getValue().c_str()); - data = ot_driver->temperatureToData(set.TSetCH); - } - else - { - msgType = OpenThermMessageType::WRITE_ACK; - // processCHSet(ot_driver->getFloat(data)); - set.TSetCH = ot_driver->getFloat(data); - set.TSetCH = constrain(set.TSetCH, conf.minCH, conf.maxCH); - // publishNew("TCHSet", String(set.TSetCH)); - IoTValue val; - val.valD = set.TSetCH; - _idSetCH->setValue(val, true); - } - break; - /* case OpenThermMessageID::MaxTSet: // максимальная уставка ГВС ?????? - if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::Hcratio: // Коэффециент тепловой кривой - if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - */ - /*----------------------------Состояние и статусы----------------------------*/ - case OpenThermMessageID::Status: // TODO Вернуть Статус котла - msgType = OpenThermMessageType::READ_ACK; - processStatus(data); - break; - case OpenThermMessageID::RelModLevel: // запрос модуляции - msgType = OpenThermMessageType::READ_ACK; - if (_idModLevel) - state.RelModLevel = ::atoi(_idModLevel->getValue().c_str()); - data = ot_driver->temperatureToData(state.RelModLevel); - break; - case OpenThermMessageID::Tboiler: // запрос температуры котла - msgType = OpenThermMessageType::READ_ACK; - if (_idTboiler) - state.Tboiler = ::atof(_idTboiler->getValue().c_str()); - data = ot_driver->temperatureToData(state.Tboiler); - break; - case OpenThermMessageID::Tdhw: // запрос температуры ГВС - msgType = OpenThermMessageType::READ_ACK; - if (_idTDhw) - { - state.Tdhw = ::atof(_idTDhw->getValue().c_str()); - data = ot_driver->temperatureToData(state.Tdhw); - } - else - { - msgType = OpenThermMessageType::UNKNOWN_DATA_ID; - } - break; - case OpenThermMessageID::Toutside: // запрос внешней температуры - msgType = OpenThermMessageType::READ_ACK; - if (_idToutside) - { - state.Toutside = ::atof(_idToutside->getValue().c_str()); - data = ot_driver->temperatureToData(state.Toutside); - } - else - { - msgType = OpenThermMessageType::UNKNOWN_DATA_ID; - } - break; - case OpenThermMessageID::ASFflags: // запрос ошибок - msgType = OpenThermMessageType::READ_ACK; - data = 0; - if (state.fl_fail) - { - data = state.fCode.service_required | (state.fCode.lockout_reset << 1) | (state.fCode.low_water_pressure << 2) | (state.fCode.gas_fault << 3) | (state.fCode.air_fault << 4) | (state.fCode.water_overtemp << 5); - data |= (uint8_t)state.fCode.fault_code << 8; - } - break; - case OpenThermMessageID::Tret: // температура обратки - msgType = OpenThermMessageType::READ_ACK; - if (_idTret) - { - state.Tret = ::atof(_idTret->getValue().c_str()); - data = ot_driver->temperatureToData(state.Tret); - } - else - { - msgType = OpenThermMessageType::UNKNOWN_DATA_ID; - } - break; - // case OpenThermMessageID::OEMDiagnosticCode: - // msgType = OpenThermMessageType::READ_ACK; - // break; - // case OpenThermMessageID::ElectricBurnerFlame: // Ток работы горелки ????? - // msgType = OpenThermMessageType::READ_ACK; - // break; - // case OpenThermMessageID::MaxCapacityMinModLevel: // максимальная мощность котла кВт и минимальная модуляция % - // msgType = OpenThermMessageType::READ_ACK; - // break; - - /*----------------------------Двусторонние информационные сообщения----------------------------*/ - /* case OpenThermMessageID::DayTime: - if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::Date: - if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::Year: - if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - // ========>>>>>>>>>>> СБРОС КОЛИЧЕСТВА 0 от мастера - case OpenThermMessageID::BurnerStarts: // Количество стартов горелки - if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::CHPumpStarts: // Количество стартов насоса СО - if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::DHWPumpValveStarts: // Количество стартов насоса/клапана ГВС - if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::DHWBurnerStarts: // Количество стартов горелки ГВС - if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::BurnerOperationHours: // часы работы горелки - if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::CHPumpOperationHours: // часы работы горелки СО - if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::DHWPumpValveOperationHours: // часы работы насоса/клапана ГВС - if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - case OpenThermMessageID::DHWBurnerOperationHours: // часы работы горелки ГВС - if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) - msgType = OpenThermMessageType::READ_ACK; - else - msgType = OpenThermMessageType::WRITE_ACK; - break; - */ - /*------------------------------------ ВСЁ ------------------------------------*/ - - default: - msgType = OpenThermMessageType::UNKNOWN_DATA_ID; - break; - } - ot_response = ot_driver->buildResponse(msgType, id, data); - ot_driver->sendResponse(ot_response); - if (_debug > 2) - { - SerialPrint("i", "OpenThermSlave ->", String(millis()) + " ID: " + String(id) + " / responseHEX: " + String(ot_response, HEX) + " / response: " + String(ot_response)); - } - } - - ~OpenThermSlave() - { - delete ot_driver; - ot_driver = nullptr; - } - }; -} - - -void *getAPI_OpenThermSlave(String subtype, String param) -{ - if (subtype == F("OpenThermSlave")) - { - return new _OpenThermSlave::OpenThermSlave(param); - } - else - { - return nullptr; - } -} diff --git a/src/modules/exec/OpenThermSlave/modinfo.json b/src/modules/exec/OpenThermSlave/modinfo.json deleted file mode 100644 index 2125a60f..00000000 --- a/src/modules/exec/OpenThermSlave/modinfo.json +++ /dev/null @@ -1,111 +0,0 @@ -{ - "menuSection": "executive_devices", - "configItem": [ - { - "global": 0, - "name": "OpenThermSlave", - "type": "Reading", - "subtype": "OpenThermSlave", - "id": "otslave", - "widget": "", - "page": "Boiler", - "descr": "Котёл", - "int": 60, - "value": "...", - "RX_pin": 13, - "TX_pin": 15, - "LogLevel": 0, - "telegram": 1, - "MemberID": 0, - "confDhw":0, - "minCH": 35, - "maxCH": 85, - "minDhw": 30, - "maxDhw": 60, - "idTboiler": "Tboiler", - "idTret": "Tret", - "idToutside": "Toutside", - "idTDhw":"TDhw", - "idStateCH":"StateCH", - "idStateDHW":"StateDHW", - "idStateFlame":"StateFlame", - "idModLevel":"ModLevel", - "idCmdCH":"CmdCH", - "idCmdDHW":"CmdDHW", - "idSetCH":"SetCH", - "idSetDHW":"SetDHW", - "idCtrlType":"CtrlType" - } - ], - "about": { - "authorName": "Mikhail Bubnov", - "authorContact": "https://t.me/Mit4bmw", - "authorGit": "https://github.com/Mit4el", - "specialThanks": "", - "moduleName": "OpenThermSlave", - "moduleVersion": "0.1", - "usedRam": { - "esp32_4mb": 15, - "esp8266_4mb": 15 - }, - "title": "OpenThermSlave", - "moduleDesc": "Модуль для автоматизации электрического котла. Мозги котла с внешним протоколом opentherm", - "propInfo": { - "int": "Интервал отправки данных в MQTT и web интерфейс", - "telegram": "1- Будет отправлять в телеграмм оповещения при ошибках котла и пропаже сигнала от котла, остальные необходимо реализовывать через сценарий", - "MemberID": "SlaveMemberIDcode - код производителя котла, кем притворится котёл;) Менять в большинстве случаев не надо", - "idPID":"ID модуля ПИД регулятора, для расчета модуляции и включения тэнов в зависимости от температуры теплоносителя, в модуле TCHSet будет уставка СО, создать TCHSet и указать его в модуле ПИД", - "idTboiler": "ID датчика температуры подачи котла", - "idTret": "ID датчика температуры обратки котла", - "idToutside": "ID датчика уличной температуры", - "rele1_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele1", - "rele2_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele2, если нет, то 0 (ноль)", - "rele3_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele3, если нет, то 0 (ноль)", - "Pupm": "1-есть реле насоса (ID реле должно называться relePump), 0-нет реле насоса, насос управляется котлом без нас", - "minCH": "Граница установки температуры СО", - "maxCH": "Граница установки температуры СО", - "gistCH": "Гистерезис СО - нагрев СО включится если температура теплоносителя ниже уставки на указанные градусы (CHSet = 45гр, gistCH = 5гр, нагрев включится когда idTboiler = 40гр)", - "idTdhw": "ID датчика температуры ГВС, например в датчик в БКН", - "idReleDhw":"ID реле трехходового крана ГВС", - "gistDhw": "Гистерезис ГВС - нагрев ГВС включится если температура воды ниже уставки на указанные градусы", - "minDhw": "Граница установки температуры ГВС", - "maxDhw": "Граница установки температуры ГВС" - }, - "funcInfo": [ - { - "name": "CHSet", - "descr": "Установить целевую температуру СО", - "params": [ - "тепмература СО (подачи) - bolier.CHSet(60)" - ] - }, - { - "name": "CHEnable", - "descr": "включить / выключить отопление", - "params": [ - "bolier.CHEnable(1) - вкл, bolier.CHEnable(0) - выкл, " - ] - }, - { - "name": "DHWSet", - "descr": "Установить целевую температуру ГВС", - "params": [ - "тепмература ГВС - dhw.DHWSet(40)" - ] - }, - { - "name": "DHWEnable", - "descr": "включить / выключить ГВС", - "params": [ - "dhw.DHWEnable(1) - вкл, dhw.DHWEnable(0) - выкл, " - ] - } - ] - }, - "defActive": true, - "usedLibs": { - "esp32_4mb3f": [], - "esp32*": [], - "esp82*": [] - } -} \ No newline at end of file diff --git a/src/modules/virtual/Benchmark/Benchmark.cpp b/src/modules/virtual/Benchmark/Benchmark.cpp new file mode 100644 index 00000000..9a3edb69 --- /dev/null +++ b/src/modules/virtual/Benchmark/Benchmark.cpp @@ -0,0 +1,165 @@ +#include "Global.h" +#include "classes/IoTBench.h" +#include +// #include + +class BenchmarkLoad : public IoTBench +{ +private: + bool _log = false; + uint32_t _loadP = 1; // период подсчета загруженности процессора + + uint32_t startLoad = 0; // время начало цикла loop + uint32_t loadPrev = 0; // время предыдущего подсчета benchmark + uint32_t loadSum = 0; // время выполнния всех циклов loop за период _loadP + float load = 0; // загруженность процессора в процентах за период _loadP (loadSum / 1000) / _loadP * 100 + uint32_t count = 0; // количестов циклов loop в сек в среднем за период _loadP + +public: + BenchmarkLoad(String parameters) : IoTBench(parameters) + { + // jsonRead(parameters, "log", _log); + // jsonRead(parameters, "int", _loadP); // в минутах + _loadP = _interval ; //* 1000 + // SerialPrint("i", "Benchmark", + // "_interval: " + String(_interval) + " _loadP: " + String(_loadP)); + if (_loadP < 10000) + _loadP = 10000; + } + + void doByInterval() + { + printBenchmarkLoad(); + } + + void loop() + { + count++; + IoTItem::loop(); + } + + void preLoadFunction() + { + startLoad = micros(); // время начала выполнения одного цикла + } + void postLoadFunction() + { + loadSum += (micros() - startLoad); // высчитываем время выполнения одного цикла (после нагрузки) и прибавляем к сумме за вреям контроля _loadP + } + + void printBenchmarkLoad() + { + load = (loadSum / 10ul) / _loadP; // (loadSum / 1000) / _loadP * 100 + + SerialPrint("i", "Benchmark", + "CPU load time: " + String(loadSum) + "us, in RealTime: " + String((micros() - loadPrev)) + "us"); + SerialPrint("i", "Benchmark", + "CPU load in " + String(_loadP) + "ms :" + String((load)) + "%" + + " loop/sec: " + String(count / (_loadP / 1000))); + loadPrev = micros(); //+= _loadP; + loadSum = 0; + count = 0; + } + + IoTBench *getBenchmarkLoad() + { + return this; + } + ~BenchmarkLoad(){ + // clearBenchConfig(); + }; +}; + +class BenchmarkTask : public IoTBench +{ +private: + uint32_t _loadP = 1; + bool _log = false; + +public: + BenchmarkTask(String parameters) : IoTBench(parameters) + { + // jsonRead(parameters, "log", _log); + // jsonRead(parameters, "int", _loadP); // в минутах + _loadP = _interval;// * 1000; + if (_loadP < 10000) + _loadP = 10000; + } + + void doByInterval() + { + printBenchmarkTask(); + } + + void preTaskFunction(const String &id) + { + if (banchItems.find(id) != banchItems.end()) + { + banchItems[id]->loopTime = micros(); // micros(); + } + else + { + banchItems[id] = new ItemBench; + banchItems[id]->loopTime = micros(); // micros(); + } + } + void postTaskFunction(const String &id) + { + if (banchItems.find(id) != banchItems.end()) + { + banchItems[id]->loopTime = micros() - banchItems[id]->loopTime; + banchItems[id]->sumloopTime += banchItems[id]->loopTime; + if (banchItems[id]->loopTime > banchItems[id]->loopTimeMax_glob) + banchItems[id]->loopTimeMax_glob = banchItems[id]->loopTime; + if (banchItems[id]->loopTime > banchItems[id]->loopTimeMax_p) + banchItems[id]->loopTimeMax_p = banchItems[id]->loopTime; + } + } + + void printBenchmarkTask() + { + for (auto it = banchItems.begin(); it != banchItems.end(); it++) + { + SerialPrint( + "i", "Benchmark", + " load (" + String((float)(it->second)->sumloopTime / 10ul / _loadP) + "%) " + + " max: per (" + String((it->second)->loopTimeMax_p) + "us)" + + " glob (" + String((it->second)->loopTimeMax_glob) + "us) - " + it->first); + (it->second)->sumloopTime = 0; + (it->second)->loopTimeMax_p = 0; + } + } + + void clearBenchConfig() + { + for (auto it = banchItems.begin(); it != banchItems.end(); it++) + { + delete it->second; + } + banchItems.clear(); + } + IoTBench *getBenchmarkTask() + { + return this; + } + ~BenchmarkTask() + { + clearBenchConfig(); + }; +}; + +void *getAPI_Benchmark(String subtype, String param) +{ + if (subtype == F("loadBench")) + { + return new BenchmarkTask(param); + } + else if (subtype == F("taskBench")) + { + return new BenchmarkLoad(param); + } + else + { + return nullptr; + } +} diff --git a/src/modules/virtual/Benchmark/modinfo.json b/src/modules/virtual/Benchmark/modinfo.json new file mode 100644 index 00000000..9ab7088c --- /dev/null +++ b/src/modules/virtual/Benchmark/modinfo.json @@ -0,0 +1,69 @@ +{ + "menuSection": "virtual_elments", + + "configItem": [ + { + "global": 0, + "name": "Load Processor", + "type": "Reading", + "subtype": "loadBench", + "id": "bench", + "needSave": 0, + "widget": "nil", + "page": "Benchmark", + "descr": "Загруженность процессора", + "int": 10, + "log": 1 + }, + { + "global": 0, + "name": "Load Task", + "type": "Reading", + "subtype": "taskBench", + "id": "bench", + "needSave": 0, + "widget": "nil", + "page": "Benchmark", + "descr": "Загруженность задач", + "int": 10, + "log": 1 + } + ], + + "about": { + "authorName": "Mikhail Bubnov", + "authorContact": "https://t.me/Mit4bmw", + "authorGit": "https://github.com/Mit4el", + "specialThanks": "", + "moduleName": "Benchmark", + "moduleVersion": "1.0", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "Производительонсть системы", + "moduleDesc": "Оценочные показатели производительности системы и выполнения модулей", + "propInfo": { + "int": "Интервал подсчета загруженности процессора в секундах" + } + }, + + "defActive": true, + + "usedLibs": { + "esp32_4mb": [], + "esp32_4mb3f": [], + "esp32s2_4mb": [], + "esp32_16mb": [], + "esp32s3_16mb": [], + "esp32c3m_4mb": [], + "esp8266_4mb": [], + "esp8266_16mb": [], + "esp8266_1mb": [], + "esp8266_1mb_ota": [], + "esp8285_1mb": [], + "esp8285_1mb_ota": [], + "esp8266_2mb": [], + "esp8266_2mb_ota": [] + } +} From 43887e74088167c1f85155feecc9333677e4102d Mon Sep 17 00:00:00 2001 From: Mit4el Date: Mon, 26 Feb 2024 23:30:04 +0300 Subject: [PATCH 03/22] Benchmark --- src/classes/IoTBench.cpp | 21 +++++++++++++++++++++ src/classes/IoTItem.cpp | 11 ++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/classes/IoTBench.cpp diff --git a/src/classes/IoTBench.cpp b/src/classes/IoTBench.cpp new file mode 100644 index 00000000..236f600b --- /dev/null +++ b/src/classes/IoTBench.cpp @@ -0,0 +1,21 @@ +#include "Global.h" +#include "classes/IoTBench.h" + +IoTBench::IoTBench(const String ¶meters) : IoTItem(parameters) +{ + /* int _tx, _rx, _speed, _line; + jsonRead(parameters, "rx", _rx); + jsonRead(parameters, "tx", _tx); + jsonRead(parameters, "speed", _speed); + jsonRead(parameters, "line", _line); + */ +} + +void IoTBench::preLoadFunction() {} +void IoTBench::postLoadFunction() {} +void IoTBench::preTaskFunction(const String &id) {} +void IoTBench::postTaskFunction(const String &id) {} + +// void IoTBench::uartHandle() {} + +IoTBench::~IoTBench() {} diff --git a/src/classes/IoTItem.cpp b/src/classes/IoTItem.cpp index ee588dc2..fd078a6f 100644 --- a/src/classes/IoTItem.cpp +++ b/src/classes/IoTItem.cpp @@ -250,7 +250,16 @@ IoTItem* IoTItem::getTlgrmDriver() { return nullptr; } -unsigned long IoTItem::getRtcUnixTime() { +IoTBench *IoTItem::getBenchmarkTask() +{ + return nullptr; +} +IoTBench *IoTItem::getBenchmarkLoad() +{ + return nullptr; +} +unsigned long IoTItem::getRtcUnixTime() +{ return 0; } From 0baf03fccfc8f983c5f343cb75a8437010fdc39c Mon Sep 17 00:00:00 2001 From: Mit4el Date: Mon, 26 Feb 2024 23:30:42 +0300 Subject: [PATCH 04/22] =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20?= =?UTF-8?q?=D0=91=D1=80=D0=BE=D0=BA=D0=B5=D1=80Mqtt=20(picoMqtt)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/exec/BrokerMQTT/BrokerMQTT.cpp | 158 +++++++++++++++++++++ src/modules/exec/BrokerMQTT/modinfo.json | 42 ++++++ 2 files changed, 200 insertions(+) create mode 100644 src/modules/exec/BrokerMQTT/BrokerMQTT.cpp create mode 100644 src/modules/exec/BrokerMQTT/modinfo.json diff --git a/src/modules/exec/BrokerMQTT/BrokerMQTT.cpp b/src/modules/exec/BrokerMQTT/BrokerMQTT.cpp new file mode 100644 index 00000000..f3595345 --- /dev/null +++ b/src/modules/exec/BrokerMQTT/BrokerMQTT.cpp @@ -0,0 +1,158 @@ +#include "Global.h" +#include "classes/IoTItem.h" +#include +#include + +namespace _Broker +{ +#define DEF_PORT 1883 + + class myPicoMQTT : public PicoMQTT::Server + { + private: + bool _debug; + + public: + myPicoMQTT(int port) : PicoMQTT::Server(port) + { + } + + void setDebug(bool debug) + { + _debug = debug; + } + + protected: + void on_connected(const char *client_id) + { + if (_debug) + { + Serial.print("[BrokerMQTT], Client connected: "); + Serial.println(client_id); + } + } + void on_disconnected(const char *client_id) + { + if (_debug) + + { + // SerialPrint("i", "BrokerMQTT", "Client disconnected: " + client_id); + Serial.print("[BrokerMQTT], Client disconnected: "); + Serial.println(client_id); + } + } + void on_subscribe(const char *client_id, const char *topic) + { + if (_debug) + + { + // SerialPrint("i", "BrokerMQTT", "Client " + client_id + ", subscribe: " + topic); + Serial.print("[BrokerMQTT], Client: "); + Serial.print(client_id); + Serial.print(", subscribe: "); + Serial.println(topic); + } + } + void on_unsubscribe(const char *client_id, const char *topic) + { + if (_debug) + + { + // SerialPrint("i", "BrokerMQTT", "Client " + client_id + ", unsubscribe: " + topic); + Serial.print("[BrokerMQTT], Client: "); + Serial.print(client_id); + Serial.print(", unsubscribe: "); + Serial.println(topic); + } + } + }; + // MqttBroker broker(1883); + myPicoMQTT *picoMqtt = nullptr; + + myPicoMQTT *instanceBroker(int port) + { + if (!picoMqtt) + { + picoMqtt = new myPicoMQTT(port); + // ot->begin(); + } + return picoMqtt; + } + + TaskHandle_t brokerTask; + // void Task2code( void * pvParameters ); + + void tBrokerMQTT(void *pvParameters) + { + TickType_t xLastWakeTime = xTaskGetTickCount(); + Serial.print("Task PicoMQTT running on core "); + Serial.println(xPortGetCoreID()); + for (;;) + { + instanceBroker(DEF_PORT)->loop(); + // picoMqtt.loop(); + // vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(5)); + } + } + + class BrokerMQTT : public IoTItem + { + private: + unsigned long ts = 0; + int _port = 0; + bool _debug; + + public: + BrokerMQTT(String parameters) : IoTItem(parameters) + { + SerialPrint("i", F("BrokerMQTT"), " START... "); + jsonRead(parameters, "port", _port); + jsonRead(parameters, "debug", _debug); + } + + void doByInterval() + { + static bool flagOne = false; + if (!flagOne) + { + if (!_port) + _port = DEF_PORT; + instanceBroker(_port)->begin(); + instanceBroker(_port)->setDebug(_debug); + // picoMqtt.begin(); + xTaskCreatePinnedToCore( + tBrokerMQTT, // Функция задачи. + "BrokerMQTT", // Имя задачи. + 10000, // Размер стека + NULL, // Параметры задачи + 0, // Приоритет + &brokerTask, // Дескриптор задачи для отслеживания + 0); + flagOne = true; + } + } + + // Основной цикл программы + void loop() + { + IoTItem::loop(); + } + + ~BrokerMQTT() + { + // delete picoMqtt; + } + }; +} + +void *getAPI_BrokerMQTT(String subtype, String param) +{ + if (subtype == F("BrokerMQTT")) + { + return new _Broker::BrokerMQTT(param); + } + else + { + return nullptr; + } +} diff --git a/src/modules/exec/BrokerMQTT/modinfo.json b/src/modules/exec/BrokerMQTT/modinfo.json new file mode 100644 index 00000000..62e38cb0 --- /dev/null +++ b/src/modules/exec/BrokerMQTT/modinfo.json @@ -0,0 +1,42 @@ +{ + "menuSection": "executive_devices", + "configItem": [ + { + "global": 0, + "name": "BrokerMQTT", + "type": "Reading", + "subtype": "BrokerMQTT", + "id": "broker", + "widget": "", + "page": "", + "descr": "", + "int": 10, + "value": "", + "port": 1883, + "debug": 1 + } + ], + "about": { + "authorName": "Mikhail Bubnov", + "authorContact": "https://t.me/Mit4bmw", + "authorGit": "https://github.com/Mit4el", + "specialThanks": "Андрей Душин", + "moduleName": "BrokerMQTT", + "moduleVersion": "0.1", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "BrokerMQTT", + "moduleDesc": "MQTT Брокер на основе Pico Mqtt", + "propInfo": { + "port":"Порт, по умолчанию 1883" + } + }, + "defActive": true, + "usedLibs": { + "esp32_4mb3f": [], + "esp32*": [], + "esp82*": [] + } +} \ No newline at end of file From 8e7d2d6a141f1d706da249ec56960a11fd5dcca0 Mon Sep 17 00:00:00 2001 From: Mit4el Date: Mon, 26 Feb 2024 23:31:12 +0300 Subject: [PATCH 05/22] =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20Sm?= =?UTF-8?q?artBoiler=20v2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/exec/SmartBoiler/BoilerHeader.h | 44 +- src/modules/exec/SmartBoiler/OpenTherm.cpp | 812 ++++++++++ src/modules/exec/SmartBoiler/OpenTherm.h | 208 +++ src/modules/exec/SmartBoiler/SmartBoiler.cpp | 1311 +++++++++++++---- src/modules/exec/SmartBoiler/modinfo.json | 79 +- src/modules/exec/SmartBoiler/readme.txt | 2 +- src/modules/exec/SmartBoiler/smartBoiler.json | 168 +-- 7 files changed, 2167 insertions(+), 457 deletions(-) create mode 100644 src/modules/exec/SmartBoiler/OpenTherm.cpp create mode 100644 src/modules/exec/SmartBoiler/OpenTherm.h diff --git a/src/modules/exec/SmartBoiler/BoilerHeader.h b/src/modules/exec/SmartBoiler/BoilerHeader.h index cda61395..5894c879 100644 --- a/src/modules/exec/SmartBoiler/BoilerHeader.h +++ b/src/modules/exec/SmartBoiler/BoilerHeader.h @@ -3,13 +3,13 @@ #define SLAVE true #define TIMEOUT_TRESHOLD 5 -namespace _Boiler -{ +namespace _Boiler_v2 +{ // команды/установки от термостата struct SetpointBoiler { - uint8_t cmd_chEnable = 0; - uint8_t cmd_dhwEnable = 0; + bool cmd_chEnable = 0; + bool cmd_dhwEnable = 0; float TSetCH = 0; float TSetDhw = 0; } set; @@ -22,33 +22,36 @@ namespace _Boiler bool gas_fault = 0; bool air_fault = 0; bool water_overtemp = 0; - uint8_t fault_code = 0; + int fault_code = 0; }; // текущее реальное состояние котла struct StateBoiler { - uint8_t stateCH = 0; - uint8_t stateDHW = 0; - uint8_t fl_flame = 0; - uint8_t currentRele = 0; - uint8_t fl_fail = 0; + bool antiFreezOn = false; + bool stateCH = 0; + bool stateDHW = 0; + bool fl_flame = 0; + int currentRele = 0; + bool fl_fail = 0; failCode fCode; float RelModLevel = 0; float Tboiler = -40; float Tret = 0; float Tdhw = 0; float Toutside = 0; - bool r[3] = {0, 0, 0}; + // bool r[3] = {0, 0, 0}; + int numStepOn; } state; // конфигурация котла struct ConfigBoiler { + bool autoPower = true; // если false то управление только из сценария или веба int antiFreez; - bool pump = false; // 1- наличие реле насоса СО, 0 - мы не управляем насосом СО (в протоколе ОТ нет) - bool changeRele = false; - // bool dhw = false; // 1- есть реле(трехходовой) ГВС + // bool pump = false; // 1- наличие реле насоса СО, 0 - мы не управляем насосом СО (в протоколе ОТ нет) + bool changeRele = false; + bool dhw = false; // 1- есть поддержка ГВС, по наличию реле(трехходовой) bool ctrlType = false; // 0 - модуляция, 1- вкл/выкл bool confDhw = false; // 1 - бак, 0 - проточная //TODO ПОКА НЕ ЗНАЮ ЧТО ДЕЛАТЬ bool pumpControlMaster = false; // в протоколе ОТ: мастер управляет насосом ????????????????????? //TODO Команды кправления насосом от мастера не помню @@ -62,17 +65,18 @@ namespace _Boiler int gistCH; int countRele = 0; - int relePwr[3]={0,0,0}; + // int relePwr[3]={0,0,0}; int prcOnekWt = 0; // процент одного киловата из общей мощности всех тэнев, расчитывается для модуляции - // int rele2Pwr = 0; - // int rele3Pwr = 0; + // int rele2Pwr = 0; + // int rele3Pwr = 0; + int numStepDhw; + float maxKW; } conf; - - unsigned long timeout_count = 0; - int _debug = 0; + bool _debug = 0; bool _telegram = false; unsigned long ot_response = 0; + int SlaveMemberIDcode = 0; } diff --git a/src/modules/exec/SmartBoiler/OpenTherm.cpp b/src/modules/exec/SmartBoiler/OpenTherm.cpp new file mode 100644 index 00000000..8f6f7467 --- /dev/null +++ b/src/modules/exec/SmartBoiler/OpenTherm.cpp @@ -0,0 +1,812 @@ +/* +OpenTherm.cpp - OpenTherm Communication Library For Arduino, ESP8266 +Copyright 2018, Ihor Melnyk +*/ + +#include "OpenTherm.h" + +OpenTherm::OpenTherm(int inPin, int outPin, bool isSlave) : status(OpenThermStatus::NOT_INITIALIZED), + inPin(inPin), + outPin(outPin), + isSlave(isSlave), + response(0), + responseStatus(OpenThermResponseStatus::NONE), + responseTimestamp(0), + handleInterruptCallback(NULL), + processResponseCallback(NULL) +{ + imitFlag = false; +} + +void OpenTherm::begin(void (*handleInterruptCallback)(void), void (*processResponseCallback)(unsigned long, OpenThermResponseStatus)) +{ + pinMode(inPin, INPUT); + pinMode(outPin, OUTPUT); + if (handleInterruptCallback != NULL) + { + this->handleInterruptCallback = handleInterruptCallback; + attachInterrupt(digitalPinToInterrupt(inPin), handleInterruptCallback, CHANGE); + } + activateBoiler(); + status = OpenThermStatus::READY; + this->processResponseCallback = processResponseCallback; +} + +void OpenTherm::begin(void (*handleInterruptCallback)(void)) +{ + begin(handleInterruptCallback, NULL); +} + +bool IRAM_ATTR OpenTherm::isReady() +{ + return status == OpenThermStatus::READY; +} + +int IRAM_ATTR OpenTherm::readState() +{ + return digitalRead(inPin); +} + +void OpenTherm::setActiveState() +{ + digitalWrite(outPin, LOW); +} + +void OpenTherm::setIdleState() +{ + digitalWrite(outPin, HIGH); +} + +void OpenTherm::activateBoiler() +{ + setIdleState(); + delay(1000); +} + +void OpenTherm::sendBit(bool high) +{ + if (high) + setActiveState(); + else + setIdleState(); + delayMicroseconds(500); + if (high) + setIdleState(); + else + setActiveState(); + delayMicroseconds(500); +} + +bool OpenTherm::sendRequestAync(unsigned long request) +{ + // Serial.println("Request: " + String(request, HEX)); + noInterrupts(); + const bool ready = isReady(); + interrupts(); + + if (!ready) + return false; + + status = OpenThermStatus::REQUEST_SENDING; + response = 0; + responseStatus = OpenThermResponseStatus::NONE; +// Prevent switching to other tasks as there is a delay within sendBit +#ifdef ESP32 +// vTaskSuspendAll(); +#endif + sendBit(HIGH); // start bit + for (int i = 31; i >= 0; i--) + { + sendBit(bitRead(request, i)); + } + sendBit(HIGH); // stop bit + setIdleState(); +#ifdef ESP32 +// xTaskResumeAll(); +#endif + status = OpenThermStatus::RESPONSE_WAITING; + responseTimestamp = micros(); + if (imitFlag) + ImitationResponse(request); + return true; +} + +unsigned long OpenTherm::sendRequest(unsigned long request) +{ + if (!sendRequestAync(request)) + return 0; + while (!isReady()) + { + process(); + yield(); + } + return response; +} + +bool OpenTherm::sendResponse(unsigned long request) +{ + status = OpenThermStatus::REQUEST_SENDING; + response = 0; + responseStatus = OpenThermResponseStatus::NONE; + // Prevent switching to other tasks as there is a delay within sendBit +#ifdef ESP32 +// vTaskSuspendAll(); +#endif + sendBit(HIGH); // start bit + for (int i = 31; i >= 0; i--) + { + sendBit(bitRead(request, i)); + } + sendBit(HIGH); // stop bit + setIdleState(); +#ifdef ESP32 +// xTaskResumeAll(); +#endif + status = OpenThermStatus::READY; + return true; +} + +unsigned long OpenTherm::getLastResponse() +{ + return response; +} + +OpenThermResponseStatus OpenTherm::getLastResponseStatus() +{ + return responseStatus; +} + +void IRAM_ATTR OpenTherm::handleInterrupt() +{ + if (isReady()) + { + if (isSlave && readState() == HIGH) + { + status = OpenThermStatus::RESPONSE_WAITING; + } + else + { + return; + } + } + + unsigned long newTs = micros(); + if (status == OpenThermStatus::RESPONSE_WAITING) + { + if (readState() == HIGH) + { + status = OpenThermStatus::RESPONSE_START_BIT; + responseTimestamp = newTs; + } + else + { + // Error start bit / Ошибка стартового бита + status = OpenThermStatus::RESPONSE_INVALID; + responseTimestamp = newTs; + } + } + else if (status == OpenThermStatus::RESPONSE_START_BIT) + { + if ((newTs - responseTimestamp < 750) && readState() == LOW) + { + status = OpenThermStatus::RESPONSE_RECEIVING; + responseTimestamp = newTs; + responseBitIndex = 0; + } + else + { + // Error Start_bit LOW 750mks / Ошибка стартового бита по тылу (нет LOW через 750мкс) + status = OpenThermStatus::RESPONSE_INVALID; + responseTimestamp = newTs; + } + } + else if (status == OpenThermStatus::RESPONSE_RECEIVING) + { + // unsigned long bitDuration = newTs - responseTimestamp; + // В новой спецификации стоповый бит не обязателен. Если не дождались, всё равно попробуем разобрать + if ((newTs - responseTimestamp) > 750 && (newTs - responseTimestamp) < 1300) + { + if (responseBitIndex < 32) + { + response = (response << 1) | !readState(); + responseTimestamp = newTs; + responseBitIndex++; + } + else + { // stop bit + status = OpenThermStatus::RESPONSE_READY; + responseTimestamp = newTs; + } + } + } +} + +void OpenTherm::process() +{ + noInterrupts(); + OpenThermStatus st = status; + unsigned long ts = responseTimestamp; + interrupts(); + + if (st == OpenThermStatus::READY) + return; + unsigned long newTs = micros(); + if (st != OpenThermStatus::NOT_INITIALIZED && st != OpenThermStatus::DELAY && (newTs - ts) > 1000000) + { + status = OpenThermStatus::READY; + responseStatus = OpenThermResponseStatus::TIMEOUT; + if (processResponseCallback != NULL) + { + processResponseCallback(response, responseStatus); + } + } + else if (st == OpenThermStatus::RESPONSE_INVALID) + { + status = OpenThermStatus::DELAY; + responseStatus = OpenThermResponseStatus::INVALID; + if (processResponseCallback != NULL) + { + processResponseCallback(response, responseStatus); + } + } + else if (st == OpenThermStatus::RESPONSE_READY) + { + status = OpenThermStatus::DELAY; + responseStatus = (isSlave ? isValidRequest(response) : isValidResponse(response)) ? OpenThermResponseStatus::SUCCESS : OpenThermResponseStatus::INVALID; + // Error msgType (READ_ACK | WRITE_ACK) is Header + if (processResponseCallback != NULL) + { + processResponseCallback(response, responseStatus); + } + } + else if (st == OpenThermStatus::DELAY) + { + if ((newTs - ts) > 100000) + { + status = OpenThermStatus::READY; + } + } +} + +bool OpenTherm::parity(unsigned long frame) // odd parity +{ + byte p = 0; + while (frame > 0) + { + if (frame & 1) + p++; + frame = frame >> 1; + } + return (p & 1); +} + +OpenThermMessageType OpenTherm::getMessageType(unsigned long message) +{ + OpenThermMessageType msg_type = static_cast((message >> 28) & 7); + return msg_type; +} + +OpenThermMessageID OpenTherm::getDataID(unsigned long frame) +{ + return (OpenThermMessageID)((frame >> 16) & 0xFF); +} + +unsigned long OpenTherm::buildRequest(OpenThermMessageType type, OpenThermMessageID id, unsigned int data) +{ + unsigned long request = data; + if (type == OpenThermMessageType::WRITE_DATA) + { + request |= 1ul << 28; + } + request |= ((unsigned long)id) << 16; + if (parity(request)) + request |= (1ul << 31); + return request; +} +unsigned long OpenTherm::buildRequestID(OpenThermMessageType type, unsigned int id, unsigned int data) +{ + unsigned long request = data; + if (type == OpenThermMessageType::WRITE_DATA) + { + request |= 1ul << 28; + } + request |= ((unsigned long)id) << 16; + if (parity(request)) + request |= (1ul << 31); + return request; +} + +unsigned long OpenTherm::buildResponse(OpenThermMessageType type, OpenThermMessageID id, unsigned int data) +{ + unsigned long response = data; + response |= ((unsigned long)type) << 28; + response |= ((unsigned long)id) << 16; + if (parity(response)) + response |= (1ul << 31); + return response; +} + +bool OpenTherm::isValidResponse(unsigned long response) +{ + if (parity(response)) + return false; + byte msgType = (response << 1) >> 29; + return msgType == READ_ACK || msgType == WRITE_ACK; +} + +bool OpenTherm::isValidRequest(unsigned long request) +{ + if (parity(request)) + return false; + byte msgType = (request << 1) >> 29; + return msgType == READ_DATA || msgType == WRITE_DATA; +} + +void OpenTherm::end() +{ + if (this->handleInterruptCallback != NULL) + { + detachInterrupt(digitalPinToInterrupt(inPin)); + } +} + +const char *OpenTherm::statusToString(OpenThermResponseStatus status) +{ + switch (status) + { + case NONE: + return "NONE"; + case SUCCESS: + return "SUCCESS"; + case INVALID: + return "INVALID"; + case TIMEOUT: + return "TIMEOUT"; + default: + return "UNKNOWN"; + } +} + +const char *OpenTherm::messageTypeToString(OpenThermMessageType message_type) +{ + switch (message_type) + { + case READ_DATA: + return "READ_DATA"; + case WRITE_DATA: + return "WRITE_DATA"; + case INVALID_DATA: + return "INVALID_DATA"; + case RESERVED: + return "RESERVED"; + case READ_ACK: + return "READ_ACK"; + case WRITE_ACK: + return "WRITE_ACK"; + case DATA_INVALID: + return "DATA_INVALID"; + case UNKNOWN_DATA_ID: + return "UNKNOWN_DATA_ID"; + default: + return "UNKNOWN"; + } +} + +// building requests + +unsigned long OpenTherm::buildSetBoilerStatusRequest(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2, bool enableSummerMode, bool dhwBlock) +{ + unsigned int data = enableCentralHeating | (enableHotWater << 1) | (enableCooling << 2) | (enableOutsideTemperatureCompensation << 3) | (enableCentralHeating2 << 4) | (enableSummerMode << 5) | (dhwBlock << 6); + data <<= 8; + return buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Status, data); +} + +unsigned long OpenTherm::buildSetBoilerTemperatureRequest(float temperature) +{ + unsigned int data = temperatureToData(temperature); + return buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TSet, data); +} + +unsigned long OpenTherm::buildGetBoilerTemperatureRequest() +{ + return buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Tboiler, 0); +} + +// parsing responses +bool OpenTherm::isFault(unsigned long response) +{ + return response & 0x1; +} + +bool OpenTherm::isCentralHeatingActive(unsigned long response) +{ + return response & 0x2; +} + +bool OpenTherm::isHotWaterActive(unsigned long response) +{ + return response & 0x4; +} + +bool OpenTherm::isFlameOn(unsigned long response) +{ + return response & 0x8; +} + +bool OpenTherm::isCoolingActive(unsigned long response) +{ + return response & 0x10; +} + +bool OpenTherm::isDiagnostic(unsigned long response) +{ + return response & 0x40; +} + +uint16_t OpenTherm::getUInt(const unsigned long response) const +{ + const uint16_t u88 = response & 0xffff; + return u88; +} + +float OpenTherm::getFloat(const unsigned long response) const +{ + const uint16_t u88 = getUInt(response); + const float f = (u88 & 0x8000) ? -(0x10000L - u88) / 256.0f : u88 / 256.0f; + return f; +} + +unsigned int OpenTherm::temperatureToData(float temperature) +{ + if (temperature < 0) + temperature = 0; + if (temperature > 100) + temperature = 100; + unsigned int data = (unsigned int)(temperature * 256); + return data; +} + +// basic requests + +unsigned long OpenTherm::setBoilerStatus(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2, bool enableSummerMode, bool dhwBlock) +{ + return sendRequest(buildSetBoilerStatusRequest(enableCentralHeating, enableHotWater, enableCooling, enableOutsideTemperatureCompensation, enableCentralHeating2, enableSummerMode, dhwBlock)); +} + +bool OpenTherm::setBoilerTemperature(float temperature) +{ + unsigned long response = sendRequest(buildSetBoilerTemperatureRequest(temperature)); + return isValidResponse(response); +} + +float OpenTherm::getBoilerTemperature() +{ + unsigned long response = sendRequest(buildGetBoilerTemperatureRequest()); + return isValidResponse(response) ? getFloat(response) : 0; +} + +float OpenTherm::getReturnTemperature() +{ + unsigned long response = sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::Tret, 0)); + return isValidResponse(response) ? getFloat(response) : 0; +} + +bool OpenTherm::setDHWSetpoint(float temperature) +{ + unsigned int data = temperatureToData(temperature); + unsigned long response = sendRequest(buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TdhwSet, data)); + return isValidResponse(response); +} + +float OpenTherm::getDHWTemperature() +{ + unsigned long response = sendRequest(buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Tdhw, 0)); + return isValidResponse(response) ? getFloat(response) : 0; +} + +float OpenTherm::getModulation() +{ + unsigned long response = sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::RelModLevel, 0)); + return isValidResponse(response) ? getFloat(response) : 0; +} + +float OpenTherm::getPressure() +{ + unsigned long response = sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::CHPressure, 0)); + return isValidResponse(response) ? getFloat(response) : 0; +} + +unsigned char OpenTherm::getFault() +{ + return ((sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::ASFflags, 0)) >> 8) & 0xff); +} +int8_t flame_timer = 0; +void OpenTherm::ImitationResponse(unsigned long request) +{ + + // unsigned long response; + unsigned int data = getUInt(request); + OpenThermMessageType msgType; + byte ID; + OpenThermMessageID id = getDataID(request); + uint8_t flags; + + switch (id) + { + case OpenThermMessageID::Status: + // Статус котла получен + msgType = OpenThermMessageType::READ_ACK; + static int8_t flame = 0; + flame_timer++; + if (flame_timer > 10) + flame = 1; + if (flame_timer > 20) + { + flame_timer = 0; + flame = 0; + } + static int8_t fault = 0; + // fault = 1 - fault; + data = (bool)fault | (true << 1) | (true << 2) | ((bool)flame << 3) | (false << 4); + break; + case OpenThermMessageID::SConfigSMemberIDcode: + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::SlaveVersion: + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::MasterVersion: + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::RelModLevel: + static float RelModLevel = 10; + // RelModLevel = RelModLevel > 100 ? 10 : RelModLevel + 1; + if (flame_timer < 11) + { + RelModLevel = 0; + } + else + { + RelModLevel = RelModLevel == 0 ? 10 : RelModLevel + 1; + } + // data = RelModLevel; + data = temperatureToData(RelModLevel); + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::Tboiler: + // Получили температуру котла + static float Tboiler = 40; + Tboiler = Tboiler > 60 ? 40 : Tboiler + 1; + data = temperatureToData(Tboiler); + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::Tdhw: + // Получили температуру ГВС + static float Tdhw = 60; + Tdhw = Tdhw > 80 ? 60 : Tdhw + 1; + data = temperatureToData(Tdhw); + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::Toutside: + // Получили внешнюю температуру + static float Toutside = -10; + Toutside = Toutside > 10 ? -10 : Toutside + 1; + data = temperatureToData(Toutside); + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::ASFflags: + msgType = OpenThermMessageType::READ_ACK; + break; + + case OpenThermMessageID::TdhwSetUBTdhwSetLB: + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::MaxTSetUBMaxTSetLB: + msgType = OpenThermMessageType::READ_ACK; + break; + + case OpenThermMessageID::OEMDiagnosticCode: + msgType = OpenThermMessageType::READ_ACK; + break; + + case OpenThermMessageID::OpenThermVersionSlave: + msgType = OpenThermMessageType::READ_ACK; + break; + + case OpenThermMessageID::CHPressure: + msgType = OpenThermMessageType::READ_ACK; + break; + + break; + case OpenThermMessageID::DHWFlowRate: + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::DayTime: + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::Date: + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::Year: + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + + case OpenThermMessageID::Tret: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::Tstorage: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::Tcollector: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::TflowCH2: + // + msgType = OpenThermMessageType::READ_ACK; + + break; + case OpenThermMessageID::Tdhw2: + // + msgType = OpenThermMessageType::READ_ACK; + + break; + case OpenThermMessageID::Texhaust: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::TheatExchanger: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::BoilerFanSpeed: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::ElectricBurnerFlame: + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::BurnerStarts: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::CHPumpStarts: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWPumpValveStarts: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWBurnerStarts: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::BurnerOperationHours: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::CHPumpOperationHours: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWPumpValveOperationHours: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWBurnerOperationHours: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::RBPflags: + // + // Pre-Defined Remote Boiler Parameters + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::TdhwSet: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::TSet: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::MaxTSet: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::Hcratio: + // + if (getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::TSP: + // + // Transparent Slave Parameters + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::FHBsize: + // + // Fault History Data + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::MaxCapacityMinModLevel: + // + // Boiler Sequencer Control + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::TrOverride: + // + // Remote override room setpoint + // + msgType = OpenThermMessageType::READ_ACK; + break; + case OpenThermMessageID::RemoteOverrideFunction: + msgType = OpenThermMessageType::READ_ACK; + break; + + default: + msgType = OpenThermMessageType::UNKNOWN_DATA_ID; + break; + } + response = buildResponse(msgType, id, data); + status = OpenThermStatus::RESPONSE_READY; + responseStatus = OpenThermResponseStatus::SUCCESS; + /* + if (processResponseCallback != NULL) + { + processResponseCallback(response, OpenThermResponseStatus::SUCCESS); + } + */ +} \ No newline at end of file diff --git a/src/modules/exec/SmartBoiler/OpenTherm.h b/src/modules/exec/SmartBoiler/OpenTherm.h new file mode 100644 index 00000000..aeea5d1b --- /dev/null +++ b/src/modules/exec/SmartBoiler/OpenTherm.h @@ -0,0 +1,208 @@ +/* +OpenTherm.h - OpenTherm Library for the ESP8266/Arduino platform +https://github.com/ihormelnyk/OpenTherm +http://ihormelnyk.com/pages/OpenTherm +Licensed under MIT license +Copyright 2018, Ihor Melnyk + +Frame Structure: +P MGS-TYPE SPARE DATA-ID DATA-VALUE +0 000 0000 00000000 00000000 00000000 +*/ + +#ifndef OpenTherm_h +#define OpenTherm_h + +#include +#include + +enum OpenThermResponseStatus : uint8_t +{ + NONE, + SUCCESS, + INVALID, + TIMEOUT +}; + +enum OpenThermMessageType : uint8_t +{ + /* Master to Slave */ + READ_DATA = B000, + READ = READ_DATA, // for backwared compatibility + WRITE_DATA = B001, + WRITE = WRITE_DATA, // for backwared compatibility + INVALID_DATA = B010, + RESERVED = B011, + /* Slave to Master */ + READ_ACK = B100, + WRITE_ACK = B101, + DATA_INVALID = B110, + UNKNOWN_DATA_ID = B111 +}; + +typedef OpenThermMessageType OpenThermRequestType; // for backwared compatibility + +enum OpenThermMessageID : uint8_t +{ + Status, // flag8 / flag8 Master and Slave Status flags. + TSet, // f8.8 Control setpoint ie CH water temperature setpoint (°C) + MConfigMMemberIDcode, // flag8 / u8 Master Configuration Flags / Master MemberID Code + SConfigSMemberIDcode, // flag8 / u8 Slave Configuration Flags / Slave MemberID Code + Command, // u8 / u8 Remote Command + ASFflags, // / OEM-fault-code flag8 / u8 Application-specific fault flags and OEM fault code + RBPflags, // flag8 / flag8 Remote boiler parameter transfer-enable & read/write flags + CoolingControl, // f8.8 Cooling control signal (%) + TsetCH2, // f8.8 Control setpoint for 2e CH circuit (°C) + TrOverride, // f8.8 Remote override room setpoint + TSP, // u8 / u8 Number of Transparent-Slave-Parameters supported by slave + TSPindexTSPvalue, // u8 / u8 Index number / Value of referred-to transparent slave parameter. + FHBsize, // u8 / u8 Size of Fault-History-Buffer supported by slave + FHBindexFHBvalue, // u8 / u8 Index number / Value of referred-to fault-history buffer entry. + MaxRelModLevelSetting, // f8.8 Maximum relative modulation level setting (%) + MaxCapacityMinModLevel, // u8 / u8 Maximum boiler capacity (kW) / Minimum boiler modulation level(%) + TrSet, // f8.8 Room Setpoint (°C) + RelModLevel, // f8.8 Relative Modulation Level (%) + CHPressure, // f8.8 Water pressure in CH circuit (bar) + DHWFlowRate, // f8.8 Water flow rate in DHW circuit. (litres/minute) + DayTime, // special / u8 Day of Week and Time of Day + Date, // u8 / u8 Calendar date + Year, // u16 Calendar year + TrSetCH2, // f8.8 Room Setpoint for 2nd CH circuit (°C) + Tr, // f8.8 Room temperature (°C) + Tboiler, // f8.8 Boiler flow water temperature (°C) + Tdhw, // f8.8 DHW temperature (°C) + Toutside, // f8.8 Outside temperature (°C) + Tret, // f8.8 Return water temperature (°C) + Tstorage, // f8.8 Solar storage temperature (°C) + Tcollector, // f8.8 Solar collector temperature (°C) + TflowCH2, // f8.8 Flow water temperature CH2 circuit (°C) + Tdhw2, // f8.8 Domestic hot water temperature 2 (°C) + Texhaust, // s16 Boiler exhaust temperature (°C) + TheatExchanger, // f8.8 Boiler heat exchanger temperature (°C) + BoilerFanSpeed, // u16 Boiler fan speed Setpiont and actual value + ElectricBurnerFlame, // f8.8?? Electric current through burner flame (mюA) + TdhwSetUBTdhwSetLB = 48, // s8 / s8 DHW setpoint upper & lower bounds for adjustment (°C) + MaxTSetUBMaxTSetLB, // s8 / s8 Max CH water setpoint upper & lower bounds for adjustment (°C) + HcratioUBHcratioLB, // s8 / s8 OTC heat curve ratio upper & lower bounds for adjustment + TdhwSet = 56, // f8.8 DHW setpoint (°C) (Remote parameter 1) + MaxTSet, // f8.8 Max CH water setpoint (°C) (Remote parameters 2) + Hcratio, // f8.8 OTC heat curve ratio (°C) (Remote parameter 3) + RemoteOverrideFunction = 100, // flag8 / - Function of manual and program changes in master and remote room setpoint. + OEMDiagnosticCode = 115, // u16 OEM-specific diagnostic/service code + BurnerStarts, // u16 Number of starts burner + CHPumpStarts, // u16 Number of starts CH pump + DHWPumpValveStarts, // u16 Number of starts DHW pump/valve + DHWBurnerStarts, // u16 Number of starts burner during DHW mode + BurnerOperationHours, // u16 Number of hours that burner is in operation (i.e. flame on) + CHPumpOperationHours, // u16 Number of hours that CH pump has been running + DHWPumpValveOperationHours, // u16 Number of hours that DHW pump has been running or DHW valve has been opened + DHWBurnerOperationHours, // u16 Number of hours that burner is in operation during DHW mode + OpenThermVersionMaster, // f8.8 The implemented version of the OpenTherm Protocol Specification in the master. + OpenThermVersionSlave, // f8.8 The implemented version of the OpenTherm Protocol Specification in the slave. + MasterVersion, // u8 / u8 Master product version number and type + SlaveVersion, // u8 / u8 Slave product version number and type +}; + +enum OpenThermStatus : uint8_t +{ + NOT_INITIALIZED, + READY, + DELAY, + REQUEST_SENDING, + RESPONSE_WAITING, + RESPONSE_START_BIT, + RESPONSE_RECEIVING, + RESPONSE_READY, + RESPONSE_INVALID +}; + +class OpenTherm +{ +public: + OpenTherm(int inPin = 4, int outPin = 5, bool isSlave = false); + volatile OpenThermStatus status; + void begin(void (*handleInterruptCallback)(void)); + void begin(void (*handleInterruptCallback)(void), void (*processResponseCallback)(unsigned long, OpenThermResponseStatus)); + bool isReady(); + unsigned long sendRequest(unsigned long request); + bool sendResponse(unsigned long request); + bool sendRequestAync(unsigned long request); + unsigned long buildRequest(OpenThermMessageType type, OpenThermMessageID id, unsigned int data); + unsigned long buildRequestID(OpenThermMessageType type, unsigned int id, unsigned int data); + unsigned long buildResponse(OpenThermMessageType type, OpenThermMessageID id, unsigned int data); + unsigned long getLastResponse(); + OpenThermResponseStatus getLastResponseStatus(); + const char *statusToString(OpenThermResponseStatus status); + void handleInterrupt(); + void process(); + void end(); + + bool parity(unsigned long frame); + OpenThermMessageType getMessageType(unsigned long message); + OpenThermMessageID getDataID(unsigned long frame); + const char *messageTypeToString(OpenThermMessageType message_type); + bool isValidRequest(unsigned long request); + bool isValidResponse(unsigned long response); + + // requests + unsigned long buildSetBoilerStatusRequest(bool enableCentralHeating, bool enableHotWater = false, bool enableCooling = false, bool enableOutsideTemperatureCompensation = false, bool enableCentralHeating2 = false, bool enableSummerMode = false, bool dhwBlock = false); + unsigned long buildSetBoilerTemperatureRequest(float temperature); + unsigned long buildGetBoilerTemperatureRequest(); + + // responses + bool isFault(unsigned long response); + bool isCentralHeatingActive(unsigned long response); + bool isHotWaterActive(unsigned long response); + bool isFlameOn(unsigned long response); + bool isCoolingActive(unsigned long response); + bool isDiagnostic(unsigned long response); + uint16_t getUInt(const unsigned long response) const; + float getFloat(const unsigned long response) const; + unsigned int temperatureToData(float temperature); + + // basic requests + unsigned long setBoilerStatus(bool enableCentralHeating, bool enableHotWater = false, bool enableCooling = false, bool enableOutsideTemperatureCompensation = false, bool enableCentralHeating2 = false, bool enableSummerMode = false, bool dhwBlock = false); + bool setBoilerTemperature(float temperature); + float getBoilerTemperature(); + float getReturnTemperature(); + bool setDHWSetpoint(float temperature); + float getDHWTemperature(); + float getModulation(); + float getPressure(); + unsigned char getFault(); + + //Имитация ответов от котла, TRUE - идет имитация ответов котла, в котел так же шлется (лучше его отключить), FALSE - штатная работа + void imitation(bool fl) {imitFlag = fl;} + +private: + bool imitFlag; + void ImitationResponse(unsigned long request); + + const int inPin; + const int outPin; + const bool isSlave; + + volatile unsigned long response; + volatile OpenThermResponseStatus responseStatus; + volatile unsigned long responseTimestamp; + volatile byte responseBitIndex; + + int readState(); + void setActiveState(); + void setIdleState(); + void activateBoiler(); + + void sendBit(bool high); + void (*handleInterruptCallback)(); + void (*processResponseCallback)(unsigned long, OpenThermResponseStatus); +}; + +#ifndef ICACHE_RAM_ATTR +#define ICACHE_RAM_ATTR +#endif + +#ifndef IRAM_ATTR +#define IRAM_ATTR ICACHE_RAM_ATTR +#endif + +#endif // OpenTherm_h diff --git a/src/modules/exec/SmartBoiler/SmartBoiler.cpp b/src/modules/exec/SmartBoiler/SmartBoiler.cpp index 15775302..ec0b74b9 100644 --- a/src/modules/exec/SmartBoiler/SmartBoiler.cpp +++ b/src/modules/exec/SmartBoiler/SmartBoiler.cpp @@ -2,10 +2,28 @@ #include "classes/IoTItem.h" #include #include "BoilerHeader.h" +#include +#include +#include "OpenTherm.h" -namespace _Boiler +#define SLAVE true +#define TIMEOUT_TRESHOLD 5 + +namespace _Boiler_v2 { - DynamicJsonDocument OpenThemData(JSON_BUFFER_SIZE / 2); + struct StepPower + { + float pwr; + std::vector listIDRele; + }; + + // std::vector vectorRele; + + std::map mapRele; + + std::map stepMap; + + // DynamicJsonDocument OpenThemData(JSON_BUFFER_SIZE / 2); IoTItem *_idPID = nullptr; IoTItem *_idTboiler = nullptr; @@ -18,31 +36,43 @@ namespace _Boiler IoTItem *_idCmdCH = nullptr; IoTItem *_idSetCH = nullptr; IoTItem *_idCtrlType = nullptr; + IoTItem *_relePump = nullptr; + + IoTItem *_idStateDHW = nullptr; + IoTItem *_idCmdDHW = nullptr; + IoTItem *_idTDhw = nullptr; + IoTItem *_idSetDHW = nullptr; + IoTItem *_releDhw = nullptr; + + float pid; + // IoTItem *rele[3]; + // IoTItem *_relePump; - int pid; - IoTItem *rele[3]; - IoTItem *relePump; - IoTItem *releDhw; IoTItem *tmp; // проверяем если пришедшее значение отличается от предыдущего регистрируем событие void publishNew(String widget, String value) { - if (OpenThemData[widget] != value) + tmp = findIoTItem(widget); + if (tmp) { - OpenThemData[widget] = value; - - tmp = findIoTItem(widget); - if (tmp) - { - tmp->setValue(value, true); - } - else - { - if (_debug > 0) - SerialPrint("new", "SmartBoiler", widget + " = " + value); - } + tmp->setValue(value, true); } + else + { + if (_debug > 0) + SerialPrint("new", "SmartBoiler", widget + " = " + value); + } + } + + // Обновит состояние реле, только если оно поменялось, что бы было меньше событий + void updateReleState(IoTItem *rele, int val) + { + if (rele) + if (val != ::atoi(rele->getValue().c_str())) + { + rele->setValue(String(val), true); + } } void sendTelegramm(String msg) @@ -54,55 +84,556 @@ namespace _Boiler } } + OpenTherm *ot_driver = nullptr; + OpenTherm *instance_OTdriver(int _RX_pin, int _TX_pin) + { + if (!ot_driver) + { + ot_driver = new OpenTherm(_RX_pin, _TX_pin, SLAVE); + // ot_driver->begin(); + } + return ot_driver; + } + + // Обработчик прерываний от ОТ + void IRAM_ATTR handleInterruptSlave() + { + if (ot_driver != nullptr) + ot_driver->handleInterrupt(); + } + + // unsigned long ot_response = 0; + // int SlaveMemberIDcode = 0; + + /* + * ========================================================================================= + * КЛАСС РАБОТЫ ПО ПРОТОКОЛУ OPENTHERM + * ========================================================================================= + */ + class OpenThermSlave : public IoTItem + { + private: + // unsigned long ts = 0; + + public: + OpenThermSlave(String parameters) : IoTItem(parameters) + { + int _RX_pin = 16; + int _TX_pin = 4; + SerialPrint("i", F("OpenThermSlave"), " START... "); + + jsonRead(parameters, "RX_pin", _RX_pin); + jsonRead(parameters, "TX_pin", _TX_pin); + jsonRead(parameters, "MemberID", SlaveMemberIDcode); + + instance_OTdriver(_RX_pin, _TX_pin); + ot_driver->begin(handleInterruptSlave, processRequest); // responseCallback + // ot_boiler = this; + } + + void configured() + { + static bool isFirstOT = true; + if (isFirstOT) + { + isFirstOT = false; + + bool haveBoiler = false; + for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) + { + if ((*it)->getSubtype() == "BoilerControl") + haveBoiler = true; + } + if (!haveBoiler) + { + SerialPrint("E", "OpenThermSlave", "Warning: BoilerControl не найден! работаем автономно по ID модулей"); + _idTboiler = findIoTItem("idTboiler"); + _idTret = findIoTItem("idTret"); + _idToutside = findIoTItem("idToutside"); + _idStateCH = findIoTItem("idStateCH"); + _idStateDHW = findIoTItem("idStateDHW"); + _idStateFlame = findIoTItem("idStateFlame"); + _idModLevel = findIoTItem("idModLevel"); + _idTDhw = findIoTItem("idTDhw"); + _idCmdCH = findIoTItem("idCmdCH"); + _idCmdDHW = findIoTItem("idCmdDHW"); + _idCtrlType = findIoTItem("idCtrlType"); + _idSetCH = findIoTItem("idSetCH"); + _idSetDHW = findIoTItem("idSetDHW"); + } + } + } + + void doByInterval() + { + configured(); + } + + // Основной цикл программы + void loop() + { + ot_driver->process(); + IoTItem::loop(); + } + + // Комманды из сценария + IoTValue execute(String command, std::vector ¶m) + { + return {}; + } + + // Обработка управления и отправка статуса + static void processStatus(unsigned int &data) + { + + uint8_t statusRequest = data >> 8; // забрали старший байт с командами мастера + set.cmd_chEnable = statusRequest & 0x1; // забрали 0 бит из этого байта - включение СО (маска 01) + + set.cmd_dhwEnable = statusRequest & 0x2; // забрали 1 бит из этого байта - включение СО (маска 10) + IoTValue val; + val.valD = set.cmd_chEnable; + if (_idCmdCH) + _idCmdCH->setValue(val, true); + val.valD = set.cmd_dhwEnable; + if (_idCmdDHW) + _idCmdDHW->setValue(val, true); + data &= 0xFF00; // старший бит не трогаем, а младший обнулили, что бы его заполнить состоянием котла и вернуть data термостату + + /* + // if (_idFail) + // state.fl_fail = ::atof(_idFail->getValue().c_str()); + if (_idStateCH) + state.stateCH = ::atoi(_idStateCH->getValue().c_str()); + if (_idStateDHW) + state.stateDHW = ::atoi(_idStateDHW->getValue().c_str()); + if (_idStateFlame) + state.fl_flame = ::atoi(_idStateFlame->getValue().c_str()); + */ + if (state.fl_fail) + data |= 0x01; // fault indication + if (state.stateCH) + data |= 0x02; // CH active + if (state.stateDHW) + data |= 0x04; // DHW active + if (state.fl_flame) + data |= 0x08; // flame on + // data |= 0x10; //cooling active + // data |= 0x20; //CH2 active + // data |= 0x40; //diagnostic/service event + // data |= 0x80; //electricity production on + } + + // обработка сброса ошибок + static void processCommand(unsigned int &data) + { + uint8_t command = data >> 8; // забрали старший байт с командами мастера + if (command == 1) + { + state.fl_fail = 0; + data |= 128; // ответ 128-255: команда выполнена + } + } + + //=================================== Обработка входящих сообщение ОТ ====================================== + static void processRequest(unsigned long request, OpenThermResponseStatus status) + { + switch (status) + { + case OpenThermResponseStatus::NONE: + if (_debug > 0) + { + SerialPrint("E", "OpenThermSlave", "Error: OpenTherm не инициализирован"); + } + break; + case OpenThermResponseStatus::INVALID: + if (_debug > 0) + { + SerialPrint("E", "OpenThermSlave", "ID:" + String(ot_driver->getDataID(request)) + " / Error: Ошибка разбора команды: " + String(request, HEX)); + // build UNKNOWN-DATAID response + unsigned long response = ot_driver->buildResponse(OpenThermMessageType::DATA_INVALID, ot_driver->getDataID(request), 0); + + // send response + ot_driver->sendResponse(response); + } + break; + case OpenThermResponseStatus::TIMEOUT: + if (_debug > 0) + { + SerialPrint("E", "OpenThermSlave", " ID: " + String(ot_driver->getDataID(request)) + " / Error: Таймаут команд от управляющего устройства (термостата)"); + } + timeout_count++; + if (timeout_count > TIMEOUT_TRESHOLD) + { + // publishNew("boilerslave", "❌"); + // publishNew("status", "не подключен"); + // setValue("ОТ не подключен", true); + timeout_count = TIMEOUT_TRESHOLD; + if (_debug > 0) + { + SerialPrint("E", "OpenThermSlave", "OpenTherm: потеря связи с управляющим устройством (термостатом) ❌"); + } + sendTelegramm(("OpenTherm: потеря связи с управляющим устройством (термостатом) ❌")); + } + break; + case OpenThermResponseStatus::SUCCESS: + timeout_count = 0; + // publishNew("boilerslave", "✅"); + // publishNew("status", "подключен"); + // setValue("ОТ подключен", true); + // sendTelegramm(("OpenTherm: котёл подключен ✅")); + // respondense_flag = true; + // ts_ = new_ts_; + HandleRequest(request); + break; + default: + break; + } + } + + // Парсинг запросов + static void HandleRequest(unsigned long request) + { + /* + if (_idCtrlType) + { + if (_idCtrlType->getValue() == "Модуляция") + conf.ctrlType = 0; + else + conf.ctrlType = 1; + } + */ + // conf.ctrlType = ::atoi(_idCtrlType->getValue().c_str()); + // unsigned long response; + unsigned int data = ot_driver->getUInt(request); + OpenThermMessageType msgType; + byte ID; + OpenThermMessageID id = ot_driver->getDataID(request); + uint8_t flags; + if (_debug > 1) + { + SerialPrint("i", "OpenThermSlave <-", String(millis()) + " ID: " + String(id) + " / requestHEX: " + String(request, HEX) + " / request: " + String(request)); + } + switch (id) + { + /*----------------------------Инициализация и конфигурация----------------------------*/ + case OpenThermMessageID::SConfigSMemberIDcode: // запрос Конфигурации Котла и SlaveMemberID + msgType = OpenThermMessageType::READ_ACK; + data = conf.dhw | (conf.ctrlType << 1) | (false << 2) | (conf.confDhw << 3) | (conf.pumpControlMaster << 4) | (false << 5); // 2-cooling, 5-CH2 + data <<= 8; + data |= SlaveMemberIDcode; + // data = (int)SlaveMemberIDcode; + break; + // case OpenThermMessageID::MConfigMMemberIDcode: // Получили Master Member ID + // msgType = OpenThermMessageType::WRITE_ACK; + // break; + // case OpenThermMessageID::SlaveVersion: // TODO вернуть версию модуля + // msgType = OpenThermMessageType::READ_ACK; + // data = (int)1; + // break; + // case OpenThermMessageID::MasterVersion: + // msgType = OpenThermMessageType::WRITE_ACK; + // break; + // case OpenThermMessageID::OpenThermVersionSlave: + // msgType = OpenThermMessageType::READ_ACK; + // break; + /*----------------------------Управление (уставки и команды)----------------------------*/ + case OpenThermMessageID::TdhwSetUBTdhwSetLB: // границы уставки ГВС + msgType = OpenThermMessageType::READ_ACK; + data |= (uint8_t)conf.minDhw; + data |= (uint8_t)conf.maxDhw << 8; + break; + case OpenThermMessageID::MaxTSetUBMaxTSetLB: // границы уставки СО + msgType = OpenThermMessageType::READ_ACK; + data |= (uint8_t)conf.minCH; + data |= (uint8_t)conf.maxCH << 8; + break; + case OpenThermMessageID::Command: // Сброс ошибок/сброс блокировки котла. Ответ: команды (не)выполнена + msgType = OpenThermMessageType::READ_ACK; + processCommand(data); + break; + case OpenThermMessageID::TdhwSet: // TODO Получили температуру ГВС + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + { + msgType = OpenThermMessageType::READ_ACK; + // if (_idSetDHW) + // set.TSetDhw = ::atof(_idSetDHW->getValue().c_str()); + data = ot_driver->temperatureToData(set.TSetDhw); + } + else + { + msgType = OpenThermMessageType::WRITE_ACK; + // processDHWSet(ot_driver->getFloat(data)); + set.TSetDhw = ot_driver->getFloat(data); + set.TSetDhw = constrain(set.TSetDhw, conf.minDhw, conf.maxDhw); + // publishNew("TDHWSet", String(set.TSetDhw)); + IoTValue val; + val.valD = set.TSetDhw; + if (_idSetDHW) + _idSetDHW->setValue(val, true); + } + break; + case OpenThermMessageID::TSet: // TODO Получили температуру СО + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + { + msgType = OpenThermMessageType::READ_ACK; + // if (_idSetCH) + // set.TSetCH = ::atof(_idSetCH->getValue().c_str()); + data = ot_driver->temperatureToData(set.TSetCH); + } + else + { + msgType = OpenThermMessageType::WRITE_ACK; + // processCHSet(ot_driver->getFloat(data)); + set.TSetCH = ot_driver->getFloat(data); + set.TSetCH = constrain(set.TSetCH, conf.minCH, conf.maxCH); + // publishNew("TCHSet", String(set.TSetCH)); + IoTValue val; + val.valD = set.TSetCH; + if (_idSetCH) + _idSetCH->setValue(val, true); + } + break; + /* case OpenThermMessageID::MaxTSet: // максимальная уставка ГВС ?????? + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::Hcratio: // Коэффециент тепловой кривой + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + */ + /*----------------------------Состояние и статусы----------------------------*/ + case OpenThermMessageID::Status: // TODO Вернуть Статус котла + msgType = OpenThermMessageType::READ_ACK; + processStatus(data); + break; + case OpenThermMessageID::RelModLevel: // запрос модуляции + msgType = OpenThermMessageType::READ_ACK; + // if (_idModLevel) + // state.RelModLevel = ::atoi(_idModLevel->getValue().c_str()); + data = ot_driver->temperatureToData(state.RelModLevel); + break; + case OpenThermMessageID::Tboiler: // запрос температуры котла + msgType = OpenThermMessageType::READ_ACK; + // if (_idTboiler) + // state.Tboiler = ::atof(_idTboiler->getValue().c_str()); + data = ot_driver->temperatureToData(state.Tboiler); + break; + case OpenThermMessageID::Tdhw: // запрос температуры ГВС + msgType = OpenThermMessageType::READ_ACK; + if (_idTDhw) + { + state.Tdhw = ::atof(_idTDhw->getValue().c_str()); + data = ot_driver->temperatureToData(state.Tdhw); + } + else + { + msgType = OpenThermMessageType::UNKNOWN_DATA_ID; + } + break; + case OpenThermMessageID::Toutside: // запрос внешней температуры + msgType = OpenThermMessageType::READ_ACK; + if (_idToutside) + { + state.Toutside = ::atof(_idToutside->getValue().c_str()); + data = ot_driver->temperatureToData(state.Toutside); + } + else + { + msgType = OpenThermMessageType::UNKNOWN_DATA_ID; + } + break; + case OpenThermMessageID::ASFflags: // запрос ошибок + msgType = OpenThermMessageType::READ_ACK; + data = 0; + if (state.fl_fail) + { + data = state.fCode.service_required | (state.fCode.lockout_reset << 1) | (state.fCode.low_water_pressure << 2) | (state.fCode.gas_fault << 3) | (state.fCode.air_fault << 4) | (state.fCode.water_overtemp << 5); + data |= (uint8_t)state.fCode.fault_code << 8; + } + break; + case OpenThermMessageID::Tret: // температура обратки + msgType = OpenThermMessageType::READ_ACK; + if (_idTret) + { + state.Tret = ::atof(_idTret->getValue().c_str()); + data = ot_driver->temperatureToData(state.Tret); + } + else + { + msgType = OpenThermMessageType::UNKNOWN_DATA_ID; + } + break; + // case OpenThermMessageID::OEMDiagnosticCode: + // msgType = OpenThermMessageType::READ_ACK; + // break; + // case OpenThermMessageID::ElectricBurnerFlame: // Ток работы горелки ????? + // msgType = OpenThermMessageType::READ_ACK; + // break; + // case OpenThermMessageID::MaxCapacityMinModLevel: // максимальная мощность котла кВт и минимальная модуляция % + // msgType = OpenThermMessageType::READ_ACK; + // break; + + /*----------------------------Двусторонние информационные сообщения----------------------------*/ + /* case OpenThermMessageID::DayTime: + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::Date: + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::Year: + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + // ========>>>>>>>>>>> СБРОС КОЛИЧЕСТВА 0 от мастера + case OpenThermMessageID::BurnerStarts: // Количество стартов горелки + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::CHPumpStarts: // Количество стартов насоса СО + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWPumpValveStarts: // Количество стартов насоса/клапана ГВС + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWBurnerStarts: // Количество стартов горелки ГВС + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::BurnerOperationHours: // часы работы горелки + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::CHPumpOperationHours: // часы работы горелки СО + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWPumpValveOperationHours: // часы работы насоса/клапана ГВС + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + case OpenThermMessageID::DHWBurnerOperationHours: // часы работы горелки ГВС + if (ot_driver->getMessageType(request) == OpenThermMessageType::READ_DATA) + msgType = OpenThermMessageType::READ_ACK; + else + msgType = OpenThermMessageType::WRITE_ACK; + break; + */ + /*------------------------------------ ВСЁ ------------------------------------*/ + + default: + msgType = OpenThermMessageType::UNKNOWN_DATA_ID; + break; + } + ot_response = ot_driver->buildResponse(msgType, id, data); + ot_driver->sendResponse(ot_response); + if (_debug > 1) + { + SerialPrint("i", "OpenThermSlave ->", String(millis()) + " ID: " + String(id) + " / responseHEX: " + String(ot_response, HEX) + " / response: " + String(ot_response)); + } + } + + ~OpenThermSlave() + { + delete ot_driver; + ot_driver = nullptr; + } + }; + /* * ========================================================================================= * КЛАСС УПРАВЛЕНИЯ ГВС * ========================================================================================= */ - IoTItem *_idTdhw = nullptr; - IoTItem *_idStateDHW = nullptr; - IoTItem *_idCmdDHW = nullptr; - IoTItem *_idTDhw = nullptr; - IoTItem *_idSetDHW = nullptr; - class DHWControl : public IoTItem { private: - unsigned long ts = 0; + // unsigned long ts = 0; public: DHWControl(String parameters) : IoTItem(parameters) { SerialPrint("i", F("DHWControl"), " START... "); - String tmpID; - jsonRead(parameters, "idReleDhw", tmpID); - releDhw = findIoTItem(tmpID); - - jsonRead(parameters, "idSetDHW", tmpID); - _idSetDHW = findIoTItem(tmpID); - jsonRead(parameters, "idStateDHW", tmpID); - _idStateDHW = findIoTItem(tmpID); - jsonRead(parameters, "idCmdDHW", tmpID); - _idCmdDHW = findIoTItem(tmpID); - jsonRead(parameters, "idTDhw", tmpID); - _idTDhw = findIoTItem(tmpID); - jsonRead(parameters, "minDhw", conf.minDhw); jsonRead(parameters, "maxDhw", conf.maxDhw); jsonRead(parameters, "gistDhw", conf.gistDhw); - // releDhw = findIoTItem("releDhw"); - if (releDhw) + jsonRead(parameters, "numStepDhw", conf.numStepDhw); + + String tmpID; + if (jsonRead(parameters, "idSetDHW", tmpID)) + _idSetDHW = findIoTItem(tmpID); + if (jsonRead(parameters, "idStateDHW", tmpID)) + _idStateDHW = findIoTItem(tmpID); + if (jsonRead(parameters, "idCmdDHW", tmpID)) + _idCmdDHW = findIoTItem(tmpID); + if (jsonRead(parameters, "idTDhw", tmpID)) + _idTDhw = findIoTItem(tmpID); + if (jsonRead(parameters, "idReleDhw", tmpID)) + _releDhw = findIoTItem(tmpID); + if (_releDhw) { SerialPrint("i", "DHWControl", "Инициализировано РЕЛЕ ГВС"); + conf.dhw = true; } - - // dhw_ctrl = this; } + /* + void configured() + { + static bool isFirstDhW = true; + if (isFirstDhW) + { + isFirstDhW = false; + String tmpID; + if (jsonRead(parameters, "idSetDHW", tmpID)) + _idSetDHW = findIoTItem(tmpID); + if (jsonRead(parameters, "idStateDHW", tmpID)) + _idStateDHW = findIoTItem(tmpID); + if (jsonRead(parameters, "idCmdDHW", tmpID)) + _idCmdDHW = findIoTItem(tmpID); + if (jsonRead(parameters, "idTDhw", tmpID)) + _idTDhw = findIoTItem(tmpID); + if (jsonRead(parameters, "idReleDhw", tmpID)) + _releDhw = findIoTItem(tmpID); + if (_releDhw){ + SerialPrint("i", "DHWControl", "Инициализировано РЕЛЕ ГВС"); + conf.dhw = true; + } + } + } + */ // ============================== ЛОГИКА РАБОТЫ КОТЛА И ВКЛЮЧЕНИЯ ТЭНОВ ====================================== // Работы котла и включения тэнов @@ -119,7 +650,7 @@ namespace _Boiler if (set.cmd_dhwEnable) { - if (releDhw) + if (_releDhw) { if (_idTDhw) state.Tdhw = ::atof(_idTDhw->getValue().c_str()); @@ -127,18 +658,20 @@ namespace _Boiler if (set.TSetDhw - state.Tdhw >= conf.gistDhw && state.Tdhw > -5 && state.Tdhw < 120) { // включаем ГВС - releDhw->setValue("1", true); + //_releDhw->setValue("1", true); + updateReleState(_releDhw, 1); state.stateDHW = 1; state.stateCH = 0; - for (uint8_t i = 0; i < conf.countRele; i++) - { - state.r[i] = true; - } - state.RelModLevel = 100; + state.numStepOn = conf.numStepDhw; + if (stepMap.find(conf.numStepDhw) != stepMap.end()) + state.RelModLevel = conf.prcOnekWt * stepMap[conf.numStepDhw].pwr; + else + state.RelModLevel = 0; } else { - releDhw->setValue("0", true); + //_releDhw->setValue("0", true); + updateReleState(_releDhw, 0); state.stateDHW = 0; state.RelModLevel = 0; } @@ -146,8 +679,9 @@ namespace _Boiler } else { - if (releDhw) - releDhw->setValue("0", true); + if (_releDhw) + //_releDhw->setValue("0", true); + updateReleState(_releDhw, 0); } // publishNew("StateDHW", String(state.stateDHW)); IoTValue val; @@ -160,6 +694,7 @@ namespace _Boiler void doByInterval() { + // configured(); } // Комманды из сценария @@ -168,21 +703,27 @@ namespace _Boiler IoTValue val; if (command == "SetDHW") { - set.TSetDhw = param[0].valD; - set.TSetDhw = constrain(set.TSetDhw, conf.minDhw, conf.maxDhw); - // publishNew("SetDHW", String(set.TSetDhw)); - val.valD = set.TSetDhw; - if (_idSetDHW) - _idSetDHW->setValue(val, true); - SerialPrint("i", "DHWControl", "Scenario DHWSet "); + if (param.size()) + { + set.TSetDhw = param[0].valD; + set.TSetDhw = constrain(set.TSetDhw, conf.minDhw, conf.maxDhw); + // publishNew("SetDHW", String(set.TSetDhw)); + val.valD = set.TSetDhw; + if (_idSetDHW) + _idSetDHW->setValue(val, true); + SerialPrint("i", "DHWControl", "Scenario DHWSet "); + } } else if (command == "DHWEnable") { - set.cmd_dhwEnable = param[0].valD; - val.valD = set.cmd_dhwEnable; - if (_idCmdDHW) - _idCmdDHW->setValue(val, true); - SerialPrint("i", "DHWControl", "Scenario DHWEnable "); + if (param.size()) + { + set.cmd_dhwEnable = param[0].valD; + val.valD = set.cmd_dhwEnable; + if (_idCmdDHW) + _idCmdDHW->setValue(val, true); + SerialPrint("i", "DHWControl", "Scenario DHWEnable "); + } } return {}; @@ -201,42 +742,17 @@ namespace _Boiler class BoilerControl : public IoTItem { private: - unsigned long ts = 0; + // unsigned long ts = 0; public: BoilerControl(String parameters) : IoTItem(parameters) { SerialPrint("i", F("BoilerControl"), " START... "); - jsonRead(parameters, "LogLevel", _debug); + jsonRead(parameters, "debug", _debug); jsonRead(parameters, "telegram", _telegram); - String tmpID; - jsonRead(parameters, "idPID", tmpID); - _idPID = findIoTItem(tmpID); - jsonRead(parameters, "idTboiler", tmpID); - _idTboiler = findIoTItem(tmpID); - jsonRead(parameters, "idTret", tmpID); - _idTret = findIoTItem(tmpID); - jsonRead(parameters, "idToutside", tmpID); - _idToutside = findIoTItem(tmpID); - jsonRead(parameters, "idStateCH", tmpID); - _idStateCH = findIoTItem(tmpID); - jsonRead(parameters, "idStateFlame", tmpID); - _idStateFlame = findIoTItem(tmpID); - jsonRead(parameters, "idModLevel", tmpID); - _idModLevel = findIoTItem(tmpID); - jsonRead(parameters, "idCmdCH", tmpID); - _idCmdCH = findIoTItem(tmpID); - jsonRead(parameters, "idSetCH", tmpID); - _idSetCH = findIoTItem(tmpID); - jsonRead(parameters, "idCtrlType", tmpID); - _idCtrlType = findIoTItem(tmpID); - - jsonRead(parameters, "rele1_Pwr", conf.relePwr[0]); - jsonRead(parameters, "rele2_Pwr", conf.relePwr[1]); - jsonRead(parameters, "rele3_Pwr", conf.relePwr[2]); - jsonRead(parameters, "Pump", conf.pump); + // jsonRead(parameters, "Pump", conf.pump); jsonRead(parameters, "changeRele", conf.changeRele); jsonRead(parameters, "minCH", conf.minCH); @@ -244,105 +760,287 @@ namespace _Boiler jsonRead(parameters, "gistCH", conf.gistCH); jsonRead(parameters, "antiFreez", conf.antiFreez); - configuration(); - } + jsonRead(parameters, "maxKW", conf.maxKW); + // configuration(); + state.fl_flame = state.stateCH = 0; + conf.countRele = conf.prcOnekWt = 0; + String tmpID; + if (jsonRead(parameters, "idPID", tmpID)) + _idPID = findIoTItem(tmpID); + if (jsonRead(parameters, "idTboiler", tmpID)) + _idTboiler = findIoTItem(tmpID); + if (jsonRead(parameters, "idTret", tmpID)) + _idTret = findIoTItem(tmpID); + if (jsonRead(parameters, "idToutside", tmpID)) + _idToutside = findIoTItem(tmpID); + + if (jsonRead(parameters, "idStateCH", tmpID)) + _idStateCH = findIoTItem(tmpID); + if (jsonRead(parameters, "idStateFlame", tmpID)) + _idStateFlame = findIoTItem(tmpID); + if (jsonRead(parameters, "idModLevel", tmpID)) + _idModLevel = findIoTItem(tmpID); + if (jsonRead(parameters, "idCmdCH", tmpID)) + _idCmdCH = findIoTItem(tmpID); + if (jsonRead(parameters, "idSetCH", tmpID)) + _idSetCH = findIoTItem(tmpID); + if (jsonRead(parameters, "idCtrlType", tmpID)) + _idCtrlType = findIoTItem(tmpID); + if (jsonRead(parameters, "idRelePump", tmpID)) + _relePump = findIoTItem(tmpID); + if (_relePump) + SerialPrint("i", "BoilerControl", "Initialized relay pump"); + + updateStateboiler(); + } + /* + void configured() + { + static bool isFirst = true; + if (isFirst) + { + isFirst = true; + + String tmpID; + if (jsonRead(parameters, "idPID", tmpID)) + _idPID = findIoTItem(tmpID); + if (jsonRead(parameters, "idTboiler", tmpID)) + _idTboiler = findIoTItem(tmpID); + if (jsonRead(parameters, "idTret", tmpID)) + _idTret = findIoTItem(tmpID); + if (jsonRead(parameters, "idToutside", tmpID)) + _idToutside = findIoTItem(tmpID); + + if (jsonRead(parameters, "idStateCH", tmpID)) + _idStateCH = findIoTItem(tmpID); + if (jsonRead(parameters, "idStateFlame", tmpID)) + _idStateFlame = findIoTItem(tmpID); + if (jsonRead(parameters, "idModLevel", tmpID)) + _idModLevel = findIoTItem(tmpID); + if (jsonRead(parameters, "idCmdCH", tmpID)) + _idCmdCH = findIoTItem(tmpID); + if (jsonRead(parameters, "idSetCH", tmpID)) + _idSetCH = findIoTItem(tmpID); + if (jsonRead(parameters, "idCtrlType", tmpID)) + _idCtrlType = findIoTItem(tmpID); + if (jsonRead(parameters, "idRelePump", tmpID)) + _relePump = findIoTItem(tmpID); + if (_relePump) + SerialPrint("i", "BoilerControl", "Initialized relay pump"); + } + } + */ // ============================== ЛОГИКА РАБОТЫ КОТЛА И ВКЛЮЧЕНИЯ ТЭНОВ ====================================== // Работы котла и включения тэнов - static void logicPowerOn() + static void + logicPowerOn() { - // TODO ВКЛЮЧЕНИЕ РЕЛЕ ПО ИХ МОЩНОСТИ (СДЕЛАТЬ ШАГИ НАГРЕВА И КОМБИНАЦИИ ВКЛБЧЕНИЯ ТЕНОВ С РАЗНОЙ МОЩНОСТЬЮ) - for (uint8_t i = 0; i < conf.countRele; i++) - { - state.r[i] = false; - } - // сейчас включаются по порядку + // TODO ВКЛЮЧЕНИЕ РЕЛЕ ПО ИХ МОЩНОСТИ (СДЕЛАТЬ ШАГИ НАГРЕВА И КОМБИНАЦИИ ВКЛБЧЕНИЯ ТЕНОВ С РАЗНОЙ МОЩНОСТЬЮ + + // state.numStepOn = 0; state.RelModLevel = 0; pid = 0; state.fl_flame = false; - if (_idPID) - pid = ::atoi(_idPID->getValue().c_str()); // обнуляем ГВС state.stateDHW = 0; + conf.countRele = mapRele.size(); // vectorRele.size(); + if (_debug > 0) + SerialPrint("I", "SmartBoiler", " countRele = " + String(conf.countRele)); + if (conf.maxKW) + { + // conf.prcOnekWt /= conf.countRele; + conf.prcOnekWt = 100 / conf.maxKW; + // SerialPrint("i", "BoilerControl", "Процент одного кВт = " + String (conf.prcOnekWt) + "%"); + } + if (conf.countRele == 1 || _idPID == nullptr) + conf.ctrlType = true; + + // IoTValue val; + // val.valD = conf.ctrlType; + if (_idCtrlType) + { + if (conf.ctrlType == 0) + _idCtrlType->setValue("Модуляция", true); + else + _idCtrlType->setValue("Вкл/Выкл", true); + } + if (_idPID) + pid = ::atof(_idPID->getValue().c_str()); + + if (conf.antiFreez > 0 && state.Tboiler < (conf.antiFreez + 5)) + { + state.antiFreezOn = true; + state.numStepOn = 1; + state.RelModLevel = conf.prcOnekWt * stepMap[1].pwr; + publishNew("status", "Анти-заморозка"); + SerialPrint("i", "BoilerControl", "Режим анти-заморозка"); + sendTelegramm("Режим анти-заморозка"); + } + else + { + if (state.antiFreezOn) + { + state.numStepOn = 0; + state.RelModLevel = 0; + state.antiFreezOn = false; + } + } + if (state.Tboiler < conf.maxCH) { - // if (dhw_ctrl) - //{ - // если есть модуль ГВС, то вызываем его логику включения тэнов - DHWControl::logicPowerDhw(); - //} - if (!state.stateDHW) // Если уже включено ГВС, то нечего смотреть на отопление + if (!state.antiFreezOn) { - if (set.cmd_chEnable) + if (conf.autoPower) { - publishNew("status", "Штатный режим"); - // включаем отопление - state.stateCH = 1; - if (state.Tboiler < (set.TSetCH + conf.gistCH) && /*set.TSetCH - state.Tboiler >= conf.gistCH &&*/ state.Tboiler > -5 && state.Tboiler < 120) + // if (dhw_ctrl) + //{ + // если есть модуль ГВС, то вызываем его логику включения тэнов + DHWControl::logicPowerDhw(); + //} + if (!state.stateDHW) // Если уже включено ГВС, то нечего смотреть на отопление { - if (conf.ctrlType == 0) - { // режим модуляции, это когда есть модуль ПИД и более одного реле + if (set.cmd_chEnable) + { + SerialPrint("I", "SmartBoiler", " stepMap.size = " + String(stepMap.size())); + publishNew("status", "Штатный режим"); + // включаем отопление + state.stateCH = 1; - if (pid > 0) - { - state.r[state.currentRele] = true; - state.RelModLevel = conf.prcOnekWt * conf.relePwr[state.currentRele]; + float TMAXcompare; + if (state.numStepOn == 0) + { // сейчас нагрев выключен + TMAXcompare = set.TSetCH - conf.gistCH; } - if (pid > 30 && pid <= 60) - { - uint8_t next = state.currentRele + 1; - if (next >= conf.countRele) - next = 0; - state.r[next] = true; - state.RelModLevel = (conf.prcOnekWt * conf.relePwr[state.currentRele]) + (conf.prcOnekWt * conf.relePwr[next]); + else + { // сейчас нагрев работает + TMAXcompare = set.TSetCH; } - if (pid > 60) + + if (state.Tboiler <= TMAXcompare && state.Tboiler > -5 && state.Tboiler < 120) { - for (uint8_t i = 0; i < conf.countRele; i++) - { - state.r[i] = true; + if (conf.ctrlType == 0) + { // режим модуляции, это когда есть модуль ПИД и более одного реле + float pidStep = 100 / stepMap.size(); + if (_debug > 0) + SerialPrint("I", "SmartBoiler", " pidStep = " + String(pidStep)); + for (size_t i = 0; i < stepMap.size(); i++) + { + if (pid > 1 && pid > i * pidStep && pid <= pidStep * (i + 1)) + { + if (_debug > 0) + { + SerialPrint("I", "SmartBoiler", " Modulation, stepON = " + String(i + 1)); + SerialPrint("I", "SmartBoiler", " Modulation, pid > " + String(i * pidStep) + "; pid <= " + String(pidStep * (i + 1))); + } + state.numStepOn = i + 1; + if (stepMap.find(state.numStepOn) != stepMap.end()) + state.RelModLevel = conf.prcOnekWt * stepMap[state.numStepOn].pwr; + else + { + state.RelModLevel = 0; + SerialPrint("E", "SmartBoiler", "НЕТ ТАКОГО Шага: " + String(state.numStepOn)); + } + } + } + if (pid >= 100) + { + if (_debug > 0) + { + SerialPrint("I", "SmartBoiler", " Modulation, stepON = " + String(stepMap.size())); + SerialPrint("I", "SmartBoiler", " Modulation, pid 100% "); + } + state.numStepOn = stepMap.size(); + if (stepMap.find(state.numStepOn) != stepMap.end()) + state.RelModLevel = 100; + else + { + state.RelModLevel = 0; + SerialPrint("E", "SmartBoiler", "НЕТ ТАКОГО Шага: " + String(state.numStepOn)); + } + } } - state.RelModLevel = 100; + else + { // у нас релейный режим без ПИДа или с одним реле(тэном) + // TODO СЕЙЧАС ВКЛЮЧАЕМ ПЕРВЫЙ ШАГ НАГРЕВА, НАДО ЧТО ТО ПРИДУМАТЬ УМНОЕ + state.numStepOn = 1; + state.RelModLevel = conf.prcOnekWt * stepMap[1].pwr; + if (_debug > 0) + SerialPrint("I", "SmartBoiler", " On|Off, stepON = " + String(state.numStepOn)); + } + } + else + { + state.numStepOn = 0; + state.RelModLevel = 0; } } else - { // у нас релейный режим без ПИДа или с одним реле(тэном) - // TODO СЕЙЧАС ВКЛЮЧАЕМ ВСЕ РЕЛЕ, НАДО ЧТО ТО ПРИДУМАТЬ УМНОЕ - for (uint8_t i = 0; i < conf.countRele; i++) - { - state.r[i] = true; - } - state.RelModLevel = 100; + { + publishNew("status", "Выкл отопление"); + state.numStepOn = 0; + state.RelModLevel = 0; + state.stateCH = 0; + if (_debug > 0) + SerialPrint("I", "SmartBoiler", " Выкл отопление "); } } - else - { - state.RelModLevel = 0; - } } else { - publishNew("status", "Выкл отопление"); - state.stateCH = 0; + publishNew("status", "Ручной режим"); + if (stepMap.find(state.numStepOn) != stepMap.end()) + state.RelModLevel = conf.prcOnekWt * stepMap[state.numStepOn].pwr; + else + { + SerialPrint("E", "SmartBoiler", "НЕТ ТАКОГО Шага: " + String(state.numStepOn)); + set.cmd_chEnable = 0; + } + + if (!set.cmd_chEnable) + { + state.numStepOn = 0; + state.RelModLevel = 0; + state.stateCH = 0; + } + if (_debug > 0) + SerialPrint("I", "SmartBoiler", " Ручной режим, stepON = " + String(state.numStepOn)); } } } - if (conf.antiFreez > 0 && state.Tboiler < (conf.antiFreez + 4)) + else { - state.r[state.currentRele] = true; - state.RelModLevel = conf.prcOnekWt * conf.relePwr[state.currentRele]; - // setValue("Анти-Заморозка"); - publishNew("status", "Режим анти-заморозка"); - SerialPrint("i", "BoilerControl", "Режим анти-заморозка"); - sendTelegramm("Режим анти-заморозка"); + state.numStepOn = 0; + state.RelModLevel = 0; + state.stateCH = 0; + SerialPrint("i", "BoilerControl", "Выключились по макс. температуре, Tboiler = " + String(state.Tboiler)); } - static bool prev_flame = false; + static bool prev_flame; - if (state.RelModLevel > 0) + if (state.numStepOn > 0) + { state.fl_flame = true; // если хоть одно реле включено, то выставляем флаг горелки + // если хоть одно реле включено, то включаем насос + if (_relePump) + //_relePump->setValue("1", true); + updateReleState(_relePump, 1); + } + else + { + // если все реле выключены + if (_relePump) + { + if (state.Tboiler > conf.minCH && set.cmd_chEnable) // НО температура ещё горячая и при этом отопление включено, то включаем насос + //_relePump->setValue("1", true); + updateReleState(_relePump, 1); + else // Если температура в котле уже остыла, или отопление нам не нужно (летом нагрели воду-пусть котел сам остывает без насоса), то выключаем насос + //_relePump->setValue("0", true); + updateReleState(_relePump, 0); + } + } if (state.fl_flame && prev_flame != state.fl_flame) { @@ -357,31 +1055,71 @@ namespace _Boiler bool fl_pump = false; - // переключаем реле в соответсии с их статусами - for (uint8_t i = 0; i < conf.countRele; i++) + // onStepPower(state.numStepOn); + if (state.numStepOn > 0) { - if (rele[i]) + if (stepMap.find(state.numStepOn) == stepMap.end()) { - rele[i]->setValue(state.r[i] ? "1" : "0", true); + SerialPrint("E", "SmartBoiler", "НЕТ ТАКОГО Шага: " + String(state.numStepOn)); + state.numStepOn = 1; + } + bool releon; + if (conf.changeRele) + { + // количество реле в текущем шаге + int countOn = stepMap[state.numStepOn].listIDRele.size(); + bool crOn = false; + // перебираем все реле + int i = 0; + for (auto it = mapRele.begin(); it != mapRele.end(); it++) + { + crOn = false; + // перебираем сколько реле нам нужно включить в текущем шаге + for (size_t c = 0; c < countOn; c++) + { + // state.currentRele - с какого реле нужно начать включение + int cr = state.currentRele + c; + if (cr >= mapRele.size()) + cr = 0; + if (i == cr) + crOn = true; + } + if (crOn) + { + updateReleState(it->second, 1); + } + else + { + updateReleState(it->second, 0); + } + i++; + } + } + else + { + for (auto it = mapRele.begin(); it != mapRele.end(); it++) + { + releon = false; + for (size_t r = 0; r < stepMap[state.numStepOn].listIDRele.size(); r++) + { + if (stepMap[state.numStepOn].listIDRele[r] == it->first) + releon = true; + } + if (releon) + updateReleState(it->second, 1); + else + updateReleState(it->second, 0); + } } - if (!fl_pump) // что бы не обнулить если выстиавили true - fl_pump = state.r[i] ? true : false; // если хоть одно реле включено, то включаем насос - } - if (fl_pump) - { // если хоть одно реле включено, то включаем насос - if (relePump) - relePump->setValue("1", true); } else { - if (relePump) - { // если все реле выключены - if (state.Tboiler > conf.minCH && set.cmd_chEnable) // НО температура ещё горячая и при этом отопление включено, то включаем насос - relePump->setValue("1", true); - else // Если температура в котле уже остыла, или отопление нам не нужно (летом нагрели воду-пусть котел сам остывает без насоса), то выключаем насос - relePump->setValue("0", true); + for (auto it = mapRele.begin(); it != mapRele.end(); it++) + { + updateReleState(it->second, 0); } } + IoTValue val; if (_idModLevel) { @@ -399,138 +1137,144 @@ namespace _Boiler _idStateFlame->setValue(val, true); } - // publishNew("ModLevel", String(state.RelModLevel)); - // publishNew("stateCH", String(state.stateCH)); - // publishNew("controlType", String(conf.ctrlType)); - // publishNew("StateFlame", String(state.fl_flame)); + publishNew("autoPower", String(conf.autoPower)); + + // setValue(String(stepMap[state.numStepOn].pwr)); + // publishNew("ModLevel", String(state.RelModLevel)); + // publishNew("StateCH", String(state.stateCH)); + // publishNew("controlType", String(conf.ctrlType)); + // publishNew("StateFlame", String(state.fl_flame)); } //============================== ОБЕСПЕЧЕНИЕ РАБОТЫ IoTMANAGER ===================================== + /* + // конфигурирование котла в зависимости от настроек + void configuration() + { + state.fl_flame = state.stateCH = 0; + conf.countRele = conf.prcOnekWt = 0; - // конфигурирование котла в зависимости от настроек - void configuration() + if (conf.pump) + { + _relePump = findIoTItem("_relePump"); + if (_relePump) + { + SerialPrint("i", "BoilerControl", "Initialized relay pump"); + } + } + + updateStateboiler(); + } + */ + void addStepPower(std::vector ¶m) { - state.fl_flame = state.stateCH = 0; - conf.countRele = conf.prcOnekWt = 0; - if (conf.relePwr[0]) + // int step = param[0].valD; + // float pwr = param[1].valD; + SerialPrint("i", "BoilerControl", "Add Step Power - " + param[0].valS + " , power - " + param[1].valS); + StepPower step; // = new StepPower; + step.pwr = param[1].valD; + for (size_t i = 2; i < param.size(); i++) { - rele[0] = findIoTItem("rele1"); - if (rele[0]) + step.listIDRele.push_back(param[i].valS); + tmp = findIoTItem(param[i].valS); + if (tmp) { - conf.countRele++; - SerialPrint("i", "BoilerControl", "Инициализировано РЕЛЕ 1-го тэна"); + // vectorRele.push_back(tmp); + mapRele[tmp->getID()] = tmp; + + SerialPrint("i", "BoilerControl", "initialized relay - " + tmp->getID()); } - } - - if (conf.relePwr[1]) - { - rele[1] = findIoTItem("rele2"); - if (rele[1]) - { - conf.countRele++; - SerialPrint("i", "BoilerControl", "Инициализировано РЕЛЕ 2-го тэна"); - } - } - - if (conf.relePwr[2]) - { - rele[2] = findIoTItem("rele3"); - if (rele[2]) - { - conf.countRele++; - SerialPrint("i", "BoilerControl", "Инициализировано РЕЛЕ 3-го тэна"); - } - } - - for (int i = 0; i < conf.countRele; i++) - { - conf.prcOnekWt += conf.relePwr[i]; - } - if (conf.countRele && conf.prcOnekWt) - { - // conf.prcOnekWt /= conf.countRele; - conf.prcOnekWt = 100 / conf.prcOnekWt; - // SerialPrint("i", "BoilerControl", "Процент одного кВт = " + String (conf.prcOnekWt) + "%"); - } - - if (conf.pump) - { - relePump = findIoTItem("relePump"); - if (relePump) - { - SerialPrint("i", "BoilerControl", "Инициализировано РЕЛЕ Насоса"); - } - } - conf.ctrlType = false; - if (conf.countRele == 1 || _idPID == nullptr) - conf.ctrlType = true; - - // IoTValue val; - // val.valD = conf.ctrlType; - if (_idCtrlType) - { - if (conf.ctrlType) - _idCtrlType->setValue("Вкл/Выкл", true); else - _idCtrlType->setValue("Модуляция", true); + { + SerialPrint("E", "BoilerControl", "Error initialized relay - " + param[i].valS); + } } - - updateStateboiler(); + stepMap[(int)param[0].valD] = step; } void doByInterval() { - // updateStateboiler(); - - // Принудительно чистим данные, что бы обновился интерфейс - OpenThemData.clear(); - - if (_debug > 0) - { - SerialPrint("i", "BoilerControl", "Обновляем данные в web интерфейсе"); - } - if (_debug > 0) - { - SerialPrint("i", "BoilerControl", "memoryUsage: " + String(OpenThemData.memoryUsage())); - } + // configured(); + updateStateboiler(); + logicPowerOn(); + if (stepMap.find(state.numStepOn) != stepMap.end()) + setValue(String(stepMap[state.numStepOn].pwr)); } - // Основной цикл программы - void loop() - { - unsigned long new_ts = millis(); - int delay = 1000; - if (new_ts - ts > delay) + /* + // Основной цикл программы + void loop() { - ts = new_ts; - updateStateboiler(); - logicPowerOn(); + unsigned long new_ts = millis(); + int delay = 1000; + if (new_ts - ts > delay) + { + ts = new_ts; + updateStateboiler(); + logicPowerOn(); + } + // для новых версий IoTManager + IoTItem::loop(); } - // для новых версий IoTManager - IoTItem::loop(); - } - + */ // Комманды из сценария IoTValue execute(String command, std::vector ¶m) { IoTValue val; if (command == "CHSet") { - set.TSetCH = param[0].valD; - set.TSetCH = constrain(set.TSetCH, conf.minCH, conf.maxCH); - val.valD = set.TSetCH; - if (_idSetCH) - _idSetCH->setValue(val, true); - // publishNew("SetCH", String(set.TSetCH)); - SerialPrint("i", "BoilerControl", "Scenario CHSet "); + if (param.size()) + { + set.TSetCH = param[0].valD; + set.TSetCH = constrain(set.TSetCH, conf.minCH, conf.maxCH); + val.valD = set.TSetCH; + if (_idSetCH) + _idSetCH->setValue(val, true); + // publishNew("SetCH", String(set.TSetCH)); + SerialPrint("i", "BoilerControl", "Scenario CHSet "); + } } else if (command == "CHEnable") { - set.cmd_chEnable = param[0].valD; - val.valD = set.cmd_chEnable; - if (_idCmdCH) - _idCmdCH->setValue(val, true); - SerialPrint("i", "BoilerControl", "Scenario CHEnable "); + if (param.size()) + { + set.cmd_chEnable = param[0].valD; + val.valD = set.cmd_chEnable; + if (_idCmdCH) + _idCmdCH->setValue(val, true); + SerialPrint("i", "BoilerControl", "Scenario CHEnable "); + } + } + else if (command == "addStepPower") + { + if (param.size() > 2) + { + // addstep(step, power, param); + addStepPower(param); + // SerialPrint("i", "BoilerControl", "Scenario addStep "); + } + } + else if (command == "onStepPower") + { + if (param.size()) + { + conf.autoPower = false; + // step[param[0].valD] = true; + // onStepPower(param[0].valD); + state.numStepOn = param[0].valD; + set.cmd_chEnable = true; + val.valD = set.cmd_chEnable; + + if (_idCmdCH) + _idCmdCH->setValue(val, true); + SerialPrint("i", "BoilerControl", "Scenario onStep, Hand Power On "); + } + } + else if (command == "autoPower") + { + + conf.autoPower = true; + SerialPrint("i", "BoilerControl", "Auto Power On"); } return {}; @@ -560,21 +1304,28 @@ namespace _Boiler ~BoilerControl() { + stepMap.clear(); + // vectorRele.clear(); + mapRele.clear(); } }; - } void *getAPI_SmartBoiler(String subtype, String param) { if (subtype == F("BoilerControl")) { - return new _Boiler::BoilerControl(param); + return new _Boiler_v2::BoilerControl(param); } else if (subtype == F("DHWControl")) { - return new _Boiler::DHWControl(param); + return new _Boiler_v2::DHWControl(param); } + else if (subtype == F("OpenThermSlave")) + { + return new _Boiler_v2::OpenThermSlave(param); + } + else { return nullptr; diff --git a/src/modules/exec/SmartBoiler/modinfo.json b/src/modules/exec/SmartBoiler/modinfo.json index 9be13ca2..75029d74 100644 --- a/src/modules/exec/SmartBoiler/modinfo.json +++ b/src/modules/exec/SmartBoiler/modinfo.json @@ -7,12 +7,12 @@ "type": "Reading", "subtype": "BoilerControl", "id": "boiler", - "widget": "anydataDef", + "widget": "anydataWt", "page": "Boiler", "descr": "Котёл", - "int": 60, + "int": 1, "value": "...", - "LogLevel": 0, + "debug": 0, "telegram": 1, "idPID":"PID", "idTboiler": "Tboiler", @@ -25,15 +25,13 @@ "idCmdDHW":"CmdDHW", "idSetCH":"SetCH", "idCtrlType":"CtrlType", - "rele1_Pwr": 1, - "rele2_Pwr": 2, - "rele3_Pwr": 4, "changeRele":0, - "Pump": 0, + "idRelePump": "relePump", "minCH": 35, "maxCH": 85, "gistCH": 5, - "antiFreez":10 + "antiFreez":10, + "maxKW": 24 }, { "global": 0, @@ -41,10 +39,10 @@ "type": "Reading", "subtype": "DHWControl", "id": "dhw", - "widget": "anydataDef", + "widget": "", "page": "Boiler", "descr": "Котёл", - "int": 60, + "int": 1, "value": "...", "idTdhw": "TDhw", "idReleDhw": "ReleDhw", @@ -53,7 +51,23 @@ "idSetDHW":"SetDHW", "minDhw": 20, "maxDhw": 60, - "gistDhw": 2 + "gistDhw": 2, + "numStepDhw":1 + }, + { + "global": 0, + "name": "OpenThermSlave", + "type": "Reading", + "subtype": "OpenThermSlave", + "id": "otslave", + "widget": "", + "page": "Boiler", + "descr": "Котёл", + "int": 1, + "value": "...", + "RX_pin": 13, + "TX_pin": 15, + "MemberID": 0 } ], "about": { @@ -62,28 +76,26 @@ "authorGit": "https://github.com/Mit4el", "specialThanks": "", "moduleName": "SmartBoiler", - "moduleVersion": "0.1", + "moduleVersion": "2.0", "usedRam": { "esp32_4mb": 15, "esp8266_4mb": 15 }, "subTypes": [ "BoilerControl", - "OpenThermSlave" + "OpenThermSlave", + "DHWControl" ], "title": "SmartBoiler", - "moduleDesc": "Модуль для автоматизации электрического котла. Мозги котла с внешним протоколом opentherm", + "moduleDesc": "Модуль для автоматизации электрического котла. Мозги котла с внешним протоколом opentherm. Модуль OpenThermSlave_v2 id модулй использует теже, что указаны в BoilerControl_v2. Но так же может работать автономно, если нет модуля BoilerControl_v2, он ищет модули по ID по умолчаию", "propInfo": { - "int": "Интервал отправки данных в MQTT и web интерфейс", + "int": "Интервал обработки логики и опроса внешних модулей", "telegram": "1- Будет отправлять в телеграмм оповещения при ошибках котла и пропаже сигнала от котла, остальные необходимо реализовывать через сценарий", "MemberID": "SlaveMemberIDcode - код производителя котла, кем притворится котёл;) Менять в большинстве случаев не надо", "idPID":"ID модуля ПИД регулятора, для расчета модуляции и включения тэнов в зависимости от температуры теплоносителя, в модуле TCHSet будет уставка СО, создать TCHSet и указать его в модуле ПИД", "idTboiler": "ID датчика температуры подачи котла", - "idTret": "ID датчика температуры обратки котла", - "idToutside": "ID датчика уличной температуры", - "rele1_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele1", - "rele2_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele2, если нет, то 0 (ноль)", - "rele3_Pwr": "Мощность тэна на первом реле, ID реле должно называться rele3, если нет, то 0 (ноль)", + "idTret": "ID датчика температуры обратки котла, только для передачи по opentherm", + "idToutside": "ID датчика уличной температуры, только для передачи по opentherm", "Pupm": "1-есть реле насоса (ID реле должно называться relePump), 0-нет реле насоса, насос управляется котлом без нас", "minCH": "Граница установки температуры СО", "maxCH": "Граница установки температуры СО", @@ -94,7 +106,9 @@ "minDhw": "Граница установки температуры ГВС", "maxDhw": "Граница установки температуры ГВС", "changeRele":"Будет менять каждый раз при включении тэн 1->2->3->1...", - "antiFreez":"Режим анти-замерзания, Указывается температура, если опустится ниже указанной, то включится нарев один тэн и нагреет на +5гр от указанной" + "antiFreez":"Режим анти-замерзания, Указывается температура, если опустится ниже указанной, то включится нарев один тэн и нагреет на +5гр от указанной", + "maxKW": "Максимальная мощность котла при включении на поcледнем Шаге Мощности", + "numStepDhw":"На каком Шаге Мощности включать ГВС" }, "funcInfo": [ { @@ -122,7 +136,28 @@ "name": "DHWEnable", "descr": "включить / выключить ГВС", "params": [ - "dhw.DHWEnable(1) - вкл, dhw.DHWEnable(0) - выкл, " + "dhw.DHWEnable(1) - вкл, dhw.DHWEnable(0) - выкл " + ] + }, + { + "name": "addStepPower", + "descr": "Добавить Шаг Нагрева: мощность Шага кВт, ID реле на данном шаге", + "params": [ + "bolier.addStepPower(1, 3, rele1) - шаг №1 в 3kW на первом реле, bolier.addStepPower(4, 24, rele1, rele3, rele4) - шаг 4 в 24Квт на 1+3+4 реле " + ] + }, + { + "name": "onStepPower", + "descr": "включить определенный шаг нагрева, указывается номер шага, Включит Ручной Режим! ", + "params": [ + "bolier.onStepPower(2) " + ] + }, + { + "name": "autoPower", + "descr": "включить автоматический режим работы котла (по умолчанию включен) ", + "params": [ + "bolier.autoPower()" ] } ] diff --git a/src/modules/exec/SmartBoiler/readme.txt b/src/modules/exec/SmartBoiler/readme.txt index 3113d406..145d8cd4 100644 --- a/src/modules/exec/SmartBoiler/readme.txt +++ b/src/modules/exec/SmartBoiler/readme.txt @@ -34,7 +34,7 @@ 3.2 Управление модулем из сценария 3.3 есть проверка ошибок датчиков (если отвалились датчика, котел не включится) 3.4 Отправка состояния в телеграмм -3.998 3.4 TODO Автоматическая отправка состояния в модули для отображения (имена модулей в логах "new") +3.998 TODO Автоматическая отправка состояния в модули для отображения (имена модулей в логах "new") 3.999 другой функционал IoTManager ... 4 Возможность управления циркуляционным насосом diff --git a/src/modules/exec/SmartBoiler/smartBoiler.json b/src/modules/exec/SmartBoiler/smartBoiler.json index 7af83ffe..201aa6f4 100644 --- a/src/modules/exec/SmartBoiler/smartBoiler.json +++ b/src/modules/exec/SmartBoiler/smartBoiler.json @@ -1,17 +1,6 @@ { "mark": "iotm", "config": [ - { - "global": 0, - "type": "Writing", - "subtype": "TelegramLT", - "id": "tg", - "widget": "", - "page": "", - "descr": "", - "token": "", - "chatID": "" - }, { "global": 0, "type": "Reading", @@ -146,7 +135,7 @@ "subtype": "Variable", "id": "ModLevel", "needSave": 0, - "widget": "anydataDef", + "widget": "anydataHum", "page": "Состояние", "descr": "Модуляция", "int": "0", @@ -168,117 +157,6 @@ "int": "0", "val": "0" }, - { - "global": 0, - "type": "Reading", - "subtype": "VButton", - "id": "ReleDhw", - "needSave": 0, - "widget": "toggle", - "page": "ГВС", - "descr": "3-хходовой", - "int": "0", - "val": "0" - }, - { - "global": 0, - "type": "Reading", - "subtype": "VButton", - "id": "CmdDHW", - "needSave": 0, - "widget": "toggle", - "page": "ГВС", - "descr": " ВКЛ ГВС", - "int": "0", - "val": "0" - }, - { - "global": 0, - "type": "Reading", - "subtype": "Variable", - "id": "TDhw", - "needSave": 0, - "widget": "inputDgt", - "page": "ГВС", - "descr": "Датчик БКН", - "int": "0", - "val": "0.0", - "map": "1024,1024,1,100", - "plus": 0, - "multiply": 1, - "round": 0 - }, - { - "global": 0, - "type": "Reading", - "subtype": "Variable", - "id": "SetDHW", - "needSave": 0, - "widget": "inputDgt", - "page": "ГВС", - "descr": "Уставка ГВС", - "int": "0", - "val": "0.0", - "map": "1024,1024,1,100", - "plus": 0, - "multiply": 1, - "round": 0 - }, - { - "global": 0, - "type": "Reading", - "subtype": "Variable", - "id": "StateDHW", - "needSave": 0, - "widget": "anydataDef", - "page": "Состояние", - "descr": "Состояние ГВС", - "int": "0", - "val": "0.0", - "map": "1024,1024,1,100", - "plus": 0, - "multiply": 1, - "round": 0 - }, - { - "global": 0, - "type": "Reading", - "subtype": "DHWControl", - "id": "dhw96", - "widget": "nil", - "page": "Boiler", - "descr": "Котёл", - "int": 60, - "value": "...", - "idTDhw": "TDhw", - "idReleDhw": "ReleDhw", - "idCmdDHW": "CmdDHW", - "idStateDHW": "StateDHW", - "idSetDHW": "SetDHW", - "minDhw": 20, - "maxDhw": 60, - "gistDhw": 2 - }, - { - "global": 0, - "needSave": 0, - "type": "Writing", - "subtype": "ThermostatPID", - "id": "PID", - "widget": "anydataHum", - "page": "Котёл", - "descr": "термостат ПИД", - "int": "10", - "round": 1, - "map": "0,100,0,100", - "set_id": "SetCH", - "term_id": "Tboiler", - "term_rezerv_id": "", - "rele": "", - "KP": 5, - "KI": 50, - "KD": 1 - }, { "global": 0, "type": "Reading", @@ -295,17 +173,37 @@ "multiply": 1, "round": 0 }, + { + "global": 0, + "needSave": 0, + "type": "Writing", + "subtype": "ThermostatPID", + "id": "PID", + "widget": "anydataHum", + "page": "Котёл", + "descr": "термостат", + "int": 60, + "round": 1, + "map": "1,100,1,100", + "set_id": "SetCH", + "term_id": "Tboiler", + "term_rezerv_id": "", + "rele": "", + "KP": 5, + "KI": 50, + "KD": 1 + }, { "global": 0, "type": "Reading", "subtype": "BoilerControl", - "id": "boiler81", - "widget": "nil", - "page": "Boiler", + "id": "boiler", + "widget": "anydataWt", + "page": "Котёл", "descr": "Котёл", - "int": 60, + "int": 1, "value": "...", - "LogLevel": 0, + "debug": "0", "telegram": 1, "idPID": "PID", "idTboiler": "Tboiler", @@ -318,20 +216,22 @@ "idCmdDHW": "CmdDHW", "idSetCH": "SetCH", "idCtrlType": "CtrlType", - "rele1_Pwr": 1, - "rele2_Pwr": 2, - "rele3_Pwr": 4, "changeRele": 0, - "Pump": 0, + "idRelePump": "relePump", "minCH": 35, "maxCH": 85, "gistCH": 5, - "antiFreez": 10 + "antiFreez": 10, + "maxKW": "8" } ] } scenario=>if onStart then { - tg.sendMsg("SmartBoiler http://" + getIP()); + boiler.addStepPower(1, 3, "rele1"); +boiler.addStepPower(2, 5, "rele2", "rele3"); +boiler.addStepPower(3, 8, "rele1", "rele2", "rele3"); +#boiler.onStepPower(2); +#boiler.autoPower(); } From b348629b0b569336db08cb8a8a7a185942f9c882 Mon Sep 17 00:00:00 2001 From: Mit4el Date: Mon, 26 Feb 2024 23:37:41 +0300 Subject: [PATCH 06/22] default profile --- myProfile.json | 14 +++++++++++++- src/modules/exec/BrokerMQTT/modinfo.json | 2 +- src/modules/exec/SmartBoiler/modinfo.json | 2 +- src/modules/virtual/Benchmark/modinfo.json | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/myProfile.json b/myProfile.json index b6246f40..2b96cb10 100644 --- a/myProfile.json +++ b/myProfile.json @@ -128,6 +128,10 @@ }, "modules": { "virtual_elments": [ + { + "path": "src/modules/virtual/Benchmark", + "active": false + }, { "path": "src/modules/virtual/Cron", "active": true @@ -290,7 +294,7 @@ "path": "src/modules/sensors/Mhz19", "active": false }, - { + { "path": "src/modules/sensors/MQgas", "active": true }, @@ -352,6 +356,10 @@ "path": "src/modules/exec/AnalogBtn", "active": true }, + { + "path": "src/modules/exec/BrokerMQTT", + "active": false + }, { "path": "src/modules/exec/ButtonIn", "active": true @@ -420,6 +428,10 @@ "path": "src/modules/exec/SDcard", "active": false }, + { + "path": "src/modules/exec/SmartBoiler", + "active": false + }, { "path": "src/modules/exec/SysExt", "active": false diff --git a/src/modules/exec/BrokerMQTT/modinfo.json b/src/modules/exec/BrokerMQTT/modinfo.json index 62e38cb0..7cad1e16 100644 --- a/src/modules/exec/BrokerMQTT/modinfo.json +++ b/src/modules/exec/BrokerMQTT/modinfo.json @@ -33,7 +33,7 @@ "port":"Порт, по умолчанию 1883" } }, - "defActive": true, + "defActive": false, "usedLibs": { "esp32_4mb3f": [], "esp32*": [], diff --git a/src/modules/exec/SmartBoiler/modinfo.json b/src/modules/exec/SmartBoiler/modinfo.json index 75029d74..af2aa194 100644 --- a/src/modules/exec/SmartBoiler/modinfo.json +++ b/src/modules/exec/SmartBoiler/modinfo.json @@ -162,7 +162,7 @@ } ] }, - "defActive": true, + "defActive": false, "usedLibs": { "esp32_4mb3f": [], "esp32*": [], diff --git a/src/modules/virtual/Benchmark/modinfo.json b/src/modules/virtual/Benchmark/modinfo.json index 9ab7088c..d33a869c 100644 --- a/src/modules/virtual/Benchmark/modinfo.json +++ b/src/modules/virtual/Benchmark/modinfo.json @@ -48,7 +48,7 @@ } }, - "defActive": true, + "defActive": false, "usedLibs": { "esp32_4mb": [], From bf7725566b01617f005fb8f44800529529eb6d38 Mon Sep 17 00:00:00 2001 From: Mit4el Date: Mon, 26 Feb 2024 23:47:09 +0300 Subject: [PATCH 07/22] upd smartBoiler --- src/modules/exec/SmartBoiler/SmartBoiler.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/exec/SmartBoiler/SmartBoiler.cpp b/src/modules/exec/SmartBoiler/SmartBoiler.cpp index ec0b74b9..6aed766c 100644 --- a/src/modules/exec/SmartBoiler/SmartBoiler.cpp +++ b/src/modules/exec/SmartBoiler/SmartBoiler.cpp @@ -1199,6 +1199,8 @@ namespace _Boiler_v2 logicPowerOn(); if (stepMap.find(state.numStepOn) != stepMap.end()) setValue(String(stepMap[state.numStepOn].pwr)); + else + setValue("0"); } /* From a63ca6e5c27cc0a81eece0d0a3da0651304daa6c Mon Sep 17 00:00:00 2001 From: Mit4el Date: Tue, 27 Feb 2024 00:02:44 +0300 Subject: [PATCH 08/22] boiler --- src/modules/exec/SmartBoiler/SmartBoiler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/exec/SmartBoiler/SmartBoiler.cpp b/src/modules/exec/SmartBoiler/SmartBoiler.cpp index 6aed766c..cd18db2d 100644 --- a/src/modules/exec/SmartBoiler/SmartBoiler.cpp +++ b/src/modules/exec/SmartBoiler/SmartBoiler.cpp @@ -902,7 +902,7 @@ namespace _Boiler_v2 //} if (!state.stateDHW) // Если уже включено ГВС, то нечего смотреть на отопление { - if (set.cmd_chEnable) + if (set.cmd_chEnable && stepMap.size()>0) { SerialPrint("I", "SmartBoiler", " stepMap.size = " + String(stepMap.size())); publishNew("status", "Штатный режим"); From ab0d5f40e2eb4247498d76182a86c0bb03ca587f Mon Sep 17 00:00:00 2001 From: Mit4el Date: Fri, 1 Mar 2024 22:00:38 +0300 Subject: [PATCH 09/22] =?UTF-8?q?=D0=9C=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20?= =?UTF-8?q?=D0=9F=D0=B8=D0=BD=D0=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/virtual/Ping/Ping.cpp | 240 ++++++++++++++++++++++ src/modules/virtual/Ping/modinfo.json | 68 ++++++ src/modules/virtual/Ping/ping_export.json | 53 +++++ 3 files changed, 361 insertions(+) create mode 100644 src/modules/virtual/Ping/Ping.cpp create mode 100644 src/modules/virtual/Ping/modinfo.json create mode 100644 src/modules/virtual/Ping/ping_export.json diff --git a/src/modules/virtual/Ping/Ping.cpp b/src/modules/virtual/Ping/Ping.cpp new file mode 100644 index 00000000..95028b0c --- /dev/null +++ b/src/modules/virtual/Ping/Ping.cpp @@ -0,0 +1,240 @@ +#include "Global.h" +#include "classes/IoTItem.h" +#include +#ifdef ESP32 +#include "sdkconfig.h" +#include "lwip/inet.h" +#include "lwip/netdb.h" +#include "lwip/sockets.h" +#include "argtable3/argtable3.h" +#include "ping/ping_sock.h" + +#endif +#ifdef ESP8266 +#include +#endif + +#ifdef ESP32 +bool pingState = false; +/* +static struct +{ + uint32_t timeout; + uint32_t interval; + uint32_t data_size; + uint32_t count; + uint32_t tos; + uint32_t ttl; + //String host; + // arg_end *end; +} ping_args; +*/ +esp_ping_config_t config = ESP_PING_DEFAULT_CONFIG(); + +static void cmd_ping_on_ping_success(esp_ping_handle_t hdl, void *args) +{ + uint8_t ttl; + uint16_t seqno; + uint32_t elapsed_time, recv_len; + ip_addr_t target_addr; + esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno)); + esp_ping_get_profile(hdl, ESP_PING_PROF_TTL, &ttl, sizeof(ttl)); + esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &target_addr, sizeof(target_addr)); + esp_ping_get_profile(hdl, ESP_PING_PROF_SIZE, &recv_len, sizeof(recv_len)); + esp_ping_get_profile(hdl, ESP_PING_PROF_TIMEGAP, &elapsed_time, sizeof(elapsed_time)); + // Serial.printf("%" PRIu32 " bytes from %s icmp_seq=%" PRIu16 " ttl=%" PRIu16 " time=%" PRIu32 " ms\n", + // recv_len, ipaddr_ntoa((ip_addr_t*)&target_addr), seqno, ttl, elapsed_time); + SerialPrint("i", "Ping", String(recv_len) + " bytes from " + String(ipaddr_ntoa((ip_addr_t *)&target_addr)) + " icmp_seq=" + String(seqno) + " ttl=" + String(ttl) + " time=" + String(elapsed_time) + " ms"); + pingState = true; +} + +static void cmd_ping_on_ping_timeout(esp_ping_handle_t hdl, void *args) +{ + uint16_t seqno; + ip_addr_t target_addr; + esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno)); + esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &target_addr, sizeof(target_addr)); + // Serial.printf("From %s icmp_seq=%d timeout\n",ipaddr_ntoa((ip_addr_t*)&target_addr), seqno); + SerialPrint("i", "Ping", "From " + String(ipaddr_ntoa((ip_addr_t *)&target_addr)) + " icmp_seq=" + String(seqno) + " timeout"); + pingState = false; +} + +static void cmd_ping_on_ping_end(esp_ping_handle_t hdl, void *args) +{ + ip_addr_t target_addr; + uint32_t transmitted; + uint32_t received; + uint32_t total_time_ms; + esp_ping_get_profile(hdl, ESP_PING_PROF_REQUEST, &transmitted, sizeof(transmitted)); + esp_ping_get_profile(hdl, ESP_PING_PROF_REPLY, &received, sizeof(received)); + esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &target_addr, sizeof(target_addr)); + esp_ping_get_profile(hdl, ESP_PING_PROF_DURATION, &total_time_ms, sizeof(total_time_ms)); + uint32_t loss = (uint32_t)((1 - ((float)received) / transmitted) * 100); + if (IP_IS_V4(&target_addr)) + { + // Serial.printf("\n--- %s ping statistics ---\n", inet_ntoa(*ip_2_ip4(&target_addr))); + SerialPrint("i", "Ping", "\n--- " + String(inet_ntoa(*ip_2_ip4(&target_addr))) + " ping statistics ---"); + } + else + { + // Serial.printf("\n--- %s ping statistics ---\n", inet6_ntoa(*ip_2_ip6(&target_addr))); + SerialPrint("i", "Ping", "\n--- " + String(inet6_ntoa(*ip_2_ip6(&target_addr))) + " ping statistics ---"); + } + // Serial.printf("%" PRIu32 " packets transmitted, %" PRIu32 " received, %" PRIu32 "%% packet loss, time %" PRIu32 "ms\n", + // transmitted, received, loss, total_time_ms); + SerialPrint("i", "Ping", String(transmitted) + " packets transmitted, " + String(received) + " received, " + String(loss) + "% packet loss, time " + String(total_time_ms) + "ms\n"); + // delete the ping sessions, so that we clean up all resources and can create a new ping session + // we don't have to call delete function in the callback, instead we can call delete function from other tasks + esp_ping_delete_session(hdl); +} +#endif +class PingIoTM : public IoTItem +{ +private: + String _ip = ""; + int timeout = 0; + int interval = 0; + int data_size = 0; + int count = 0; + int tos = 0; + int ttl = 0; + +public: + PingIoTM(String parameters) : IoTItem(parameters) + { + jsonRead(parameters, "ip", _ip); + jsonRead(parameters, "timeout", timeout); + jsonRead(parameters, "interval", interval); + jsonRead(parameters, "data_size", data_size); + jsonRead(parameters, "count", count); + jsonRead(parameters, "tos", tos); + jsonRead(parameters, "ttl", ttl); + +#ifdef ESP32 +/* + ping_args.timeout = 10; // arg_dbl0("W", "timeout", "", "Time to wait for a response, in seconds"); + ping_args.interval = 1; // arg_dbl0("i", "interval", "", "Wait interval seconds between sending each packet"); + ping_args.data_size = 0; // arg_int0("s", "size", "", "Specify the number of data bytes to be sent"); + ping_args.count = 0; // arg_int0("c", "count", "", "Stop after sending count packets"); + ping_args.tos = 0; // arg_int0("Q", "tos", "", "Set Type of Service related bits in IP datagrams"); + ping_args.ttl = 0; // arg_int0("T", "ttl", "", "Set Time to Live related bits in IP datagrams"); + // ping_args.end = arg_end(1); +*/ + if (timeout > 0) + config.timeout_ms = (uint32_t)(timeout * 1000); + if (interval > 0) + config.interval_ms = (uint32_t)(interval * 1000); + if (data_size > 0) + config.data_size = (uint32_t)(data_size); + if (count > 0) + config.count = (uint32_t)(count); + if (tos > 0) + config.tos = (uint32_t)(tos); + if (ttl > 0) + config.ttl = (uint32_t)(ttl); +#endif + } + + void doByInterval() + { +#ifdef ESP8266 + regEvent((float)Ping.ping(_ip.c_str()), "ping"); +#endif + } + + // Основной цикл программы + void loop() + { +#ifdef ESP32 + if (value.valD != (float)pingState) + regEvent((float)pingState, "ping"); + +#endif + IoTItem::loop(); + } + + IoTValue execute(String command, std::vector ¶m) + { + IoTValue val; + if (command == "ping") + { +#ifdef ESP32 + if (param.size()) + { + // parse IP address + struct sockaddr_in6 sock_addr6; + ip_addr_t target_addr; + memset(&target_addr, 0, sizeof(target_addr)); + + if (inet_pton(AF_INET6, _ip.c_str(), &sock_addr6.sin6_addr) == 1) + { + /* convert ip6 string to ip6 address */ + ipaddr_aton(_ip.c_str(), &target_addr); + } + else + { + struct addrinfo hint; + struct addrinfo *res = NULL; + memset(&hint, 0, sizeof(hint)); + /* convert ip4 string or hostname to ip4 or ip6 address */ + if (getaddrinfo(_ip.c_str(), NULL, &hint, &res) != 0) + { + // Serial.printf("ping: unknown host %s\n", ping_args.host.c_str()); + SerialPrint("E", "Ping", "ping: unknown host " + _ip); + // return 1; + } + if (res->ai_family == AF_INET) + { + struct in_addr addr4 = ((struct sockaddr_in *)(res->ai_addr))->sin_addr; + inet_addr_to_ip4addr(ip_2_ip4(&target_addr), &addr4); + } + else + { + struct in6_addr addr6 = ((struct sockaddr_in6 *)(res->ai_addr))->sin6_addr; + inet6_addr_to_ip6addr(ip_2_ip6(&target_addr), &addr6); + } + freeaddrinfo(res); + } + config.target_addr = target_addr; + /* set callback functions */ + esp_ping_callbacks_t cbs = { + .cb_args = NULL, + .on_ping_success = cmd_ping_on_ping_success, + .on_ping_timeout = cmd_ping_on_ping_timeout, + .on_ping_end = cmd_ping_on_ping_end}; + esp_ping_handle_t ping; + + esp_ping_new_session(&config, &cbs, &ping); + esp_ping_start(ping); + } +#endif +#ifdef ESP8266 + if (param.size()) + { + val.valD = Ping.ping(param[0].valS.c_str()); + if (val.valD) + SerialPrint("I", "Ping", "Ping success"); + else + SerialPrint("E", "Ping", "Ping error"); + regEvent(val, "ping"); + return val; + } +#endif + } + return {}; + } + + ~PingIoTM(){}; +}; + +void *getAPI_Ping(String subtype, String param) +{ + if (subtype == F("Ping")) + { + return new PingIoTM(param); + } + else + { + return nullptr; + } +} diff --git a/src/modules/virtual/Ping/modinfo.json b/src/modules/virtual/Ping/modinfo.json new file mode 100644 index 00000000..df8e6ca9 --- /dev/null +++ b/src/modules/virtual/Ping/modinfo.json @@ -0,0 +1,68 @@ +{ + "menuSection": "virtual_elments", + + "configItem": [ + { + "global": 0, + "name": "Ping", + "type": "Reading", + "subtype": "Ping", + "id": "ping", + "needSave": 0, + "widget": "nil", + "page": "", + "descr": "", + "ip": "8.8.8.8", + "timeout": 5, + "interval": 1, + "data_size": 0, + "count": 0, + "tos": 0, + "ttl": 0 + } + ], + + "about": { + "authorName": "Mikhail Bubnov", + "authorContact": "https://t.me/Mit4bmw", + "authorGit": "https://github.com/Mit4el", + "specialThanks": "", + "moduleName": "Ping", + "moduleVersion": "1.0", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "Пинг", + "moduleDesc": "Пинг - проверка доступности сетевого адреса", + "propInfo": { + "ip": "IP адрес (8.8.8.8) или имя хоста (www.google.com) кого пингуем", + "timeout": "Тайм-аут. Только для ESP32. Если 0, то пингует со значением по умолчанию", + "interval": "Интервал. Только для ESP32. Если 0, то пингует со значением по умолчанию", + "data_size": "Размер пакета. Только для ESP32. Если 0, то пингует со значением по умолчанию", + "count": "Количество пакетов. Только для ESP32. Если 0, то пингует со значением по умолчанию", + "tos": "Type Of Service. Только для ESP32. Если 0, то пингует со значением по умолчанию", + "ttl": "Time To Live. Только для ESP32. Если 0, то пингует со значением по умолчанию" + }, + "funcInfo": [ + { + "name": "ping", + "descr": "проверить пинг вручную из сценария", + "params": [ + "IP адрес или имя хоста" + ] + } + ] + }, + + "defActive": true, + + "usedLibs": { + "esp32*": [ + + ], + "esp82*": [ + "https://github.com/dancol90/ESP8266Ping" + ] + } +} diff --git a/src/modules/virtual/Ping/ping_export.json b/src/modules/virtual/Ping/ping_export.json new file mode 100644 index 00000000..80631305 --- /dev/null +++ b/src/modules/virtual/Ping/ping_export.json @@ -0,0 +1,53 @@ +{ + "mark": "iotm", + "config": [ + { + "global": 0, + "type": "Reading", + "subtype": "VButton", + "id": "vbtn67", + "needSave": 0, + "widget": "toggle", + "page": "Пинг", + "descr": "ping", + "int": "0", + "val": "0" + }, + { + "global": 0, + "type": "Reading", + "subtype": "Variable", + "id": "vout96", + "needSave": 0, + "widget": "anydataDef", + "page": "Пинг", + "descr": "Состояние", + "int": "0", + "val": "...", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0 + }, + { + "global": 0, + "type": "Reading", + "subtype": "Ping", + "id": "ping21", + "needSave": 0, + "widget": "anydataDef", + "page": "Пинг", + "descr": "Статус", + "ip": "8.8.8.8", + "timeout": 5, + "interval": 1, + "data_size": 0, + "count": 0, + "tos": 0, + "ttl": 0 + } + ] +} + +scenario=>if vbtn67==1 then ping21.ping("8.8.8.8") +if ping21==0 then vout96 = "нет интернета" else vout96 ="есть интернет" \ No newline at end of file From d2865118f460713441b32f33fe732f93db1bc166 Mon Sep 17 00:00:00 2001 From: Mit4el Date: Tue, 12 Mar 2024 00:11:45 +0300 Subject: [PATCH 10/22] =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20Pi?= =?UTF-8?q?ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/virtual/Ping/Ping.cpp | 14 +++++++------- src/modules/virtual/Ping/modinfo.json | 10 ++++------ src/modules/virtual/Ping/ping_export.json | 3 +-- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/modules/virtual/Ping/Ping.cpp b/src/modules/virtual/Ping/Ping.cpp index 95028b0c..47d21153 100644 --- a/src/modules/virtual/Ping/Ping.cpp +++ b/src/modules/virtual/Ping/Ping.cpp @@ -97,7 +97,7 @@ private: int data_size = 0; int count = 0; int tos = 0; - int ttl = 0; +// int ttl = 0; //Есть только в новых версиях framwork ПОКА УБРАЛ public: PingIoTM(String parameters) : IoTItem(parameters) @@ -108,7 +108,7 @@ public: jsonRead(parameters, "data_size", data_size); jsonRead(parameters, "count", count); jsonRead(parameters, "tos", tos); - jsonRead(parameters, "ttl", ttl); + // jsonRead(parameters, "ttl", ttl); #ifdef ESP32 /* @@ -130,18 +130,18 @@ public: config.count = (uint32_t)(count); if (tos > 0) config.tos = (uint32_t)(tos); - if (ttl > 0) - config.ttl = (uint32_t)(ttl); + // if (ttl > 0) + // config.ttl = (uint32_t)(ttl); #endif } - +/* void doByInterval() { #ifdef ESP8266 regEvent((float)Ping.ping(_ip.c_str()), "ping"); #endif } - +*/ // Основной цикл программы void loop() { @@ -216,7 +216,7 @@ public: SerialPrint("I", "Ping", "Ping success"); else SerialPrint("E", "Ping", "Ping error"); - regEvent(val, "ping"); + regEvent(val.valD, "ping"); return val; } #endif diff --git a/src/modules/virtual/Ping/modinfo.json b/src/modules/virtual/Ping/modinfo.json index df8e6ca9..4a809ae7 100644 --- a/src/modules/virtual/Ping/modinfo.json +++ b/src/modules/virtual/Ping/modinfo.json @@ -17,8 +17,7 @@ "interval": 1, "data_size": 0, "count": 0, - "tos": 0, - "ttl": 0 + "tos": 0 } ], @@ -28,7 +27,7 @@ "authorGit": "https://github.com/Mit4el", "specialThanks": "", "moduleName": "Ping", - "moduleVersion": "1.0", + "moduleVersion": "1.2", "usedRam": { "esp32_4mb": 15, "esp8266_4mb": 15 @@ -41,13 +40,12 @@ "interval": "Интервал. Только для ESP32. Если 0, то пингует со значением по умолчанию", "data_size": "Размер пакета. Только для ESP32. Если 0, то пингует со значением по умолчанию", "count": "Количество пакетов. Только для ESP32. Если 0, то пингует со значением по умолчанию", - "tos": "Type Of Service. Только для ESP32. Если 0, то пингует со значением по умолчанию", - "ttl": "Time To Live. Только для ESP32. Если 0, то пингует со значением по умолчанию" + "tos": "Type Of Service. Только для ESP32. Если 0, то пингует со значением по умолчанию" }, "funcInfo": [ { "name": "ping", - "descr": "проверить пинг вручную из сценария", + "descr": "Проверить пинг. после вызова данной функции из сценария результат будет в значении самого модуля. if ping21==1 then ЕСТЬ_пинг else НЕТ_пинга", "params": [ "IP адрес или имя хоста" ] diff --git a/src/modules/virtual/Ping/ping_export.json b/src/modules/virtual/Ping/ping_export.json index 80631305..e36d1065 100644 --- a/src/modules/virtual/Ping/ping_export.json +++ b/src/modules/virtual/Ping/ping_export.json @@ -43,8 +43,7 @@ "interval": 1, "data_size": 0, "count": 0, - "tos": 0, - "ttl": 0 + "tos": 0 } ] } From d00eabbe0a7784da7a4edb4c171f0ccce57a4af3 Mon Sep 17 00:00:00 2001 From: Mit4el Date: Tue, 12 Mar 2024 00:13:09 +0300 Subject: [PATCH 11/22] BrokerMqtt: add lib PicoMqtt --- lib/PicoMQTT/src/PicoMQTT.h | 20 + lib/PicoMQTT/src/PicoMQTT/autoid.h | 21 + lib/PicoMQTT/src/PicoMQTT/client.cpp | 288 ++++++++++++++ lib/PicoMQTT/src/PicoMQTT/client.h | 82 ++++ lib/PicoMQTT/src/PicoMQTT/client_wrapper.cpp | 119 ++++++ lib/PicoMQTT/src/PicoMQTT/client_wrapper.h | 31 ++ lib/PicoMQTT/src/PicoMQTT/config.h | 29 ++ lib/PicoMQTT/src/PicoMQTT/connection.cpp | 176 +++++++++ lib/PicoMQTT/src/PicoMQTT/connection.h | 80 ++++ lib/PicoMQTT/src/PicoMQTT/debug.h | 50 +++ lib/PicoMQTT/src/PicoMQTT/incoming_packet.cpp | 171 ++++++++ lib/PicoMQTT/src/PicoMQTT/incoming_packet.h | 46 +++ lib/PicoMQTT/src/PicoMQTT/outgoing_packet.cpp | 224 +++++++++++ lib/PicoMQTT/src/PicoMQTT/outgoing_packet.h | 64 +++ lib/PicoMQTT/src/PicoMQTT/packet.h | 49 +++ lib/PicoMQTT/src/PicoMQTT/pico_interface.h | 13 + lib/PicoMQTT/src/PicoMQTT/print_mux.cpp | 29 ++ lib/PicoMQTT/src/PicoMQTT/print_mux.h | 29 ++ lib/PicoMQTT/src/PicoMQTT/publisher.cpp | 56 +++ lib/PicoMQTT/src/PicoMQTT/publisher.h | 90 +++++ lib/PicoMQTT/src/PicoMQTT/server.cpp | 365 ++++++++++++++++++ lib/PicoMQTT/src/PicoMQTT/server.h | 105 +++++ lib/PicoMQTT/src/PicoMQTT/subscriber.cpp | 174 +++++++++ lib/PicoMQTT/src/PicoMQTT/subscriber.h | 73 ++++ src/modules/exec/BrokerMQTT/modinfo.json | 3 +- 25 files changed, 2385 insertions(+), 2 deletions(-) create mode 100644 lib/PicoMQTT/src/PicoMQTT.h create mode 100644 lib/PicoMQTT/src/PicoMQTT/autoid.h create mode 100644 lib/PicoMQTT/src/PicoMQTT/client.cpp create mode 100644 lib/PicoMQTT/src/PicoMQTT/client.h create mode 100644 lib/PicoMQTT/src/PicoMQTT/client_wrapper.cpp create mode 100644 lib/PicoMQTT/src/PicoMQTT/client_wrapper.h create mode 100644 lib/PicoMQTT/src/PicoMQTT/config.h create mode 100644 lib/PicoMQTT/src/PicoMQTT/connection.cpp create mode 100644 lib/PicoMQTT/src/PicoMQTT/connection.h create mode 100644 lib/PicoMQTT/src/PicoMQTT/debug.h create mode 100644 lib/PicoMQTT/src/PicoMQTT/incoming_packet.cpp create mode 100644 lib/PicoMQTT/src/PicoMQTT/incoming_packet.h create mode 100644 lib/PicoMQTT/src/PicoMQTT/outgoing_packet.cpp create mode 100644 lib/PicoMQTT/src/PicoMQTT/outgoing_packet.h create mode 100644 lib/PicoMQTT/src/PicoMQTT/packet.h create mode 100644 lib/PicoMQTT/src/PicoMQTT/pico_interface.h create mode 100644 lib/PicoMQTT/src/PicoMQTT/print_mux.cpp create mode 100644 lib/PicoMQTT/src/PicoMQTT/print_mux.h create mode 100644 lib/PicoMQTT/src/PicoMQTT/publisher.cpp create mode 100644 lib/PicoMQTT/src/PicoMQTT/publisher.h create mode 100644 lib/PicoMQTT/src/PicoMQTT/server.cpp create mode 100644 lib/PicoMQTT/src/PicoMQTT/server.h create mode 100644 lib/PicoMQTT/src/PicoMQTT/subscriber.cpp create mode 100644 lib/PicoMQTT/src/PicoMQTT/subscriber.h diff --git a/lib/PicoMQTT/src/PicoMQTT.h b/lib/PicoMQTT/src/PicoMQTT.h new file mode 100644 index 00000000..7fa7f667 --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +#if defined(ESP8266) + +#if (ARDUINO_ESP8266_MAJOR != 3) || (ARDUINO_ESP8266_MINOR < 1) +#error PicoMQTT requires ESP8266 board core version >= 3.1 +#endif + +#elif defined(ESP32) + +//#if ESP_ARDUINO_VERSION < ESP_ARDUINO_VERSION_VAL(2, 0, 7) +//#error PicoMQTT requires ESP32 board core version >= 2.0.7 +//#endif + +#endif + +#include "PicoMQTT/client.h" +#include "PicoMQTT/server.h" diff --git a/lib/PicoMQTT/src/PicoMQTT/autoid.h b/lib/PicoMQTT/src/PicoMQTT/autoid.h new file mode 100644 index 00000000..a3b17643 --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/autoid.h @@ -0,0 +1,21 @@ +#pragma once + +namespace PicoMQTT { + +class AutoId { + public: + typedef unsigned int Id; + + AutoId(): id(generate_id()) {} + AutoId(const AutoId &) = default; + + const Id id; + + private: + static Id generate_id() { + static Id next_id = 1; + return next_id++; + } +}; + +} diff --git a/lib/PicoMQTT/src/PicoMQTT/client.cpp b/lib/PicoMQTT/src/PicoMQTT/client.cpp new file mode 100644 index 00000000..ee242826 --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/client.cpp @@ -0,0 +1,288 @@ +#include "client.h" +#include "debug.h" + +namespace PicoMQTT { + +BasicClient::BasicClient(unsigned long keep_alive_seconds, unsigned long socket_timeout_seconds) + : Connection(keep_alive_seconds, socket_timeout_seconds) { + TRACE_FUNCTION +} + +BasicClient::BasicClient(const ::WiFiClient & client, unsigned long keep_alive_seconds, + unsigned long socket_timeout_seconds) + : Connection(client, keep_alive_seconds, socket_timeout_seconds) { + TRACE_FUNCTION +} + +bool BasicClient::connect( + const char * host, + uint16_t port, + const char * id, + const char * user, + const char * pass, + const char * will_topic, + const char * will_message, + const size_t will_message_length, + uint8_t will_qos, + bool will_retain, + const bool clean_session, + ConnectReturnCode * connect_return_code) { + TRACE_FUNCTION + + if (connect_return_code) { + *connect_return_code = CRC_UNDEFINED; + } + + client.stop(); + + if (!client.connect(host, port)) { + return false; + } + + message_id_generator.reset(); + + const bool will = will_topic && will_message; + + const uint8_t connect_flags = + (user ? 1 : 0) << 7 + | (user && pass ? 1 : 0) << 6 + | (will && will_retain ? 1 : 0) << 5 + | (will && will_qos ? 1 : 0) << 3 + | (will ? 1 : 0) << 2 + | (clean_session ? 1 : 0) << 1; + + const size_t client_id_length = strlen(id); + const size_t will_topic_length = (will && will_topic) ? strlen(will_topic) : 0; + const size_t user_length = user ? strlen(user) : 0; + const size_t pass_length = pass ? strlen(pass) : 0; + + const size_t total_size = 6 // protocol name + + 1 // protocol level + + 1 // connect flags + + 2 // keep-alive + + client_id_length + 2 + + (will ? will_topic_length + 2 : 0) + + (will ? will_message_length + 2 : 0) + + (user ? user_length + 2 : 0) + + (user && pass ? pass_length + 2 : 0); + + auto packet = build_packet(Packet::CONNECT, 0, total_size); + packet.write_string("MQTT", 4); + packet.write_u8(4); + packet.write_u8(connect_flags); + packet.write_u16(keep_alive_millis / 1000); + packet.write_string(id, client_id_length); + + if (will) { + packet.write_string(will_topic, will_topic_length); + packet.write_string(will_message, will_message_length); + } + + if (user) { + packet.write_string(user, user_length); + if (pass) { + packet.write_string(pass, pass_length); + } + } + + if (!packet.send()) { + return false; + } + + wait_for_reply(Packet::CONNACK, [this, connect_return_code](IncomingPacket & packet) { + TRACE_FUNCTION + if (packet.size != 2) { + on_protocol_violation(); + return; + } + + /* const uint8_t connect_ack_flags = */ packet.read_u8(); + const uint8_t crc = packet.read_u8(); + + if (connect_return_code) { + *connect_return_code = (ConnectReturnCode) crc; + } + + if (crc != 0) { + // connection refused + client.stop(); + } + }); + + return client.connected(); +} + +void BasicClient::loop() { + TRACE_FUNCTION + + if (client.connected() && get_millis_since_last_write() >= keep_alive_millis) { + // ping time! + build_packet(Packet::PINGREQ).send(); + wait_for_reply(Packet::PINGRESP, [](IncomingPacket &) {}); + } + + Connection::loop(); +} + +Publisher::Publish BasicClient::begin_publish(const char * topic, const size_t payload_size, + uint8_t qos, bool retain, uint16_t message_id) { + TRACE_FUNCTION + return Publish( + *this, + client.status() ? client : PrintMux(), + topic, payload_size, + (qos >= 1) ? 1 : 0, + retain, + message_id, // dup if message_id is non-zero + message_id ? message_id : message_id_generator.generate() // generate only if message_id == 0 + ); +} + +bool BasicClient::on_publish_complete(const Publish & publish) { + TRACE_FUNCTION + if (publish.qos == 0) { + return true; + } + + bool confirmed = false; + wait_for_reply(Packet::PUBACK, [&publish, &confirmed](IncomingPacket & puback) { + confirmed |= (puback.read_u16() == publish.message_id); + }); + + return confirmed; +} + +bool BasicClient::subscribe(const String & topic, uint8_t qos, uint8_t * qos_granted) { + TRACE_FUNCTION + if (qos > 1) { + return false; + } + + const size_t topic_size = topic.length(); + const uint16_t message_id = message_id_generator.generate(); + + auto packet = build_packet(Packet::SUBSCRIBE, 0b0010, 2 + 2 + topic_size + 1); + packet.write_u16(message_id); + packet.write_string(topic.c_str(), topic_size); + packet.write_u8(qos); + packet.send(); + + uint8_t code = 0x80; + + wait_for_reply(Packet::SUBACK, [this, message_id, &code](IncomingPacket & packet) { + if (packet.read_u16() != message_id) { + on_protocol_violation(); + } else { + code = packet.read_u8(); + } + }); + + if (code == 0x80) { + return false; + } + + if (qos_granted) { + *qos_granted = code; + } + + return client.connected(); +} + +bool BasicClient::unsubscribe(const String & topic) { + TRACE_FUNCTION + + const size_t topic_size = topic.length(); + const uint16_t message_id = message_id_generator.generate(); + + auto packet = build_packet(Packet::UNSUBSCRIBE, 0b0010, 2 + 2 + topic_size); + packet.write_u16(message_id); + packet.write_string(topic.c_str(), topic_size); + packet.send(); + + wait_for_reply(Packet::UNSUBACK, [this, message_id](IncomingPacket & packet) { + if (packet.read_u16() != message_id) { + on_protocol_violation(); + } + }); + + return client.connected(); +} + +Client::Client(const char * host, uint16_t port, const char * id, const char * user, const char * password, + unsigned long reconnect_interval_millis) + : host(host), port(port), client_id(id), username(user), password(password), + will({"", "", 0, false}), +reconnect_interval_millis(reconnect_interval_millis), +last_reconnect_attempt(millis() - reconnect_interval_millis) { + TRACE_FUNCTION +} + +Client::SubscriptionId Client::subscribe(const String & topic_filter, MessageCallback callback) { + TRACE_FUNCTION + BasicClient::subscribe(topic_filter); + return SubscribedMessageListener::subscribe(topic_filter, callback); +} + +void Client::unsubscribe(const String & topic_filter) { + TRACE_FUNCTION + BasicClient::unsubscribe(topic_filter); + SubscribedMessageListener::unsubscribe(topic_filter); +} + +void Client::on_message(const char * topic, IncomingPacket & packet) { + SubscribedMessageListener::fire_message_callbacks(topic, packet); +} + +void Client::loop() { + TRACE_FUNCTION + if (!client.connected()) { + if (host.isEmpty() || !port) { + return; + } + + if (millis() - last_reconnect_attempt < reconnect_interval_millis) { + return; + } + + const bool connection_established = connect(host.c_str(), port, + client_id.isEmpty() ? "" : client_id.c_str(), + username.isEmpty() ? nullptr : username.c_str(), + password.isEmpty() ? nullptr : password.c_str(), + will.topic.isEmpty() ? nullptr : will.topic.c_str(), + will.payload.isEmpty() ? nullptr : will.payload.c_str(), + will.payload.isEmpty() ? 0 : will.payload.length(), + will.qos, will.retain); + + last_reconnect_attempt = millis(); + + if (!connection_established) { + return; + } + + for (const auto & kv : subscriptions) { + BasicClient::subscribe(kv.first.c_str()); + } + + on_connect(); + } + + BasicClient::loop(); +} + +void Client::on_connect() { + TRACE_FUNCTION + BasicClient::on_connect(); + if (connected_callback) { + connected_callback(); + } +} + +void Client::on_disconnect() { + TRACE_FUNCTION + BasicClient::on_disconnect(); + if (disconnected_callback) { + connected_callback(); + } +} + +} diff --git a/lib/PicoMQTT/src/PicoMQTT/client.h b/lib/PicoMQTT/src/PicoMQTT/client.h new file mode 100644 index 00000000..4d0ab542 --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/client.h @@ -0,0 +1,82 @@ +#pragma once + +#include + +#include "connection.h" +#include "incoming_packet.h" +#include "outgoing_packet.h" +#include "pico_interface.h" +#include "publisher.h" +#include "subscriber.h" + +namespace PicoMQTT { + +class BasicClient: public PicoMQTTInterface, public Connection, public Publisher { + public: + BasicClient(unsigned long keep_alive_seconds = 60, unsigned long socket_timeout_seconds = 10); + + BasicClient(const ::WiFiClient & client, unsigned long keep_alive_seconds = 60, + unsigned long socket_timeout_seconds = 10); + + bool connect( + const char * host, uint16_t port = 1883, + const char * id = "", const char * user = nullptr, const char * pass = nullptr, + const char * will_topic = nullptr, const char * will_message = nullptr, + const size_t will_message_length = 0, uint8_t willQos = 0, bool willRetain = false, + const bool cleanSession = true, + ConnectReturnCode * connect_return_code = nullptr); + + using Publisher::begin_publish; + virtual Publish begin_publish(const char * topic, const size_t payload_size, + uint8_t qos = 0, bool retain = false, uint16_t message_id = 0) override; + + bool subscribe(const String & topic, uint8_t qos = 0, uint8_t * qos_granted = nullptr); + bool unsubscribe(const String & topic); + + void loop() override; + + virtual void on_connect() {} + + private: + virtual bool on_publish_complete(const Publish & publish) override; +}; + +class Client: public BasicClient, public SubscribedMessageListener { + public: + Client(const char * host = nullptr, uint16_t port = 1883, const char * id = nullptr, const char * user = nullptr, + const char * password = nullptr, unsigned long reconnect_interval_millis = 5 * 1000); + + using SubscribedMessageListener::subscribe; + virtual SubscriptionId subscribe(const String & topic_filter, MessageCallback callback) override; + virtual void unsubscribe(const String & topic_filter) override; + + virtual void loop() override; + + String host; + uint16_t port; + + String client_id; + String username; + String password; + + struct { + String topic; + String payload; + uint8_t qos; + bool retain; + } will; + + unsigned long reconnect_interval_millis; + + std::function connected_callback; + std::function disconnected_callback; + + virtual void on_connect() override; + virtual void on_disconnect() override; + + protected: + unsigned long last_reconnect_attempt; + virtual void on_message(const char * topic, IncomingPacket & packet) override; +}; + +} diff --git a/lib/PicoMQTT/src/PicoMQTT/client_wrapper.cpp b/lib/PicoMQTT/src/PicoMQTT/client_wrapper.cpp new file mode 100644 index 00000000..c2c37b1a --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/client_wrapper.cpp @@ -0,0 +1,119 @@ +#include "Arduino.h" + +#include "client_wrapper.h" + +#include "debug.h" + +namespace PicoMQTT { + +ClientWrapper::ClientWrapper(unsigned long socket_timeout_seconds): socket_timeout_millis( + socket_timeout_seconds * 1000) { + TRACE_FUNCTION +} + +ClientWrapper::ClientWrapper(const ::WiFiClient & client, unsigned long socket_timeout_seconds): + WiFiClient(client), socket_timeout_millis(socket_timeout_seconds * 1000) { + TRACE_FUNCTION +} + +// reads +int ClientWrapper::available_wait(unsigned long timeout) { + TRACE_FUNCTION + const unsigned long start_millis = millis(); + while (true) { + const int ret = available(); + if (ret > 0) { + return ret; + } + if (!status()) { + // A disconnected client might still have unread data waiting in buffers. Don't move this check earlier. + return 0; + } + const unsigned long elapsed = millis() - start_millis; + if (elapsed > timeout) { + return 0; + } + yield(); + } +} + +int ClientWrapper::read(uint8_t * buf, size_t size) { + TRACE_FUNCTION + const unsigned long start_millis = millis(); + size_t ret = 0; + + while (ret < size) { + const unsigned long now_millis = millis(); + const unsigned long elapsed_millis = now_millis - start_millis; + + if (elapsed_millis > socket_timeout_millis) { + // timeout + abort(); + break; + } + + const unsigned long remaining_millis = socket_timeout_millis - elapsed_millis; + + const int available_size = available_wait(remaining_millis); + if (available_size <= 0) { + // timeout + abort(); + break; + } + + const int chunk_size = size - ret < (size_t) available_size ? size - ret : (size_t) available_size; + + const int bytes_read = WiFiClient::read(buf + ret, chunk_size); + if (bytes_read <= 0) { + // connection error + abort(); + break; + } + + ret += bytes_read; + } + + return ret; +} + +int ClientWrapper::read() { + TRACE_FUNCTION + if (!available_wait(socket_timeout_millis)) { + return -1; + } + return WiFiClient::read(); +} + +int ClientWrapper::peek() { + TRACE_FUNCTION + if (!available_wait(socket_timeout_millis)) { + return -1; + } + return WiFiClient::peek(); +} + +// writes +size_t ClientWrapper::write(const uint8_t * buffer, size_t size) { + TRACE_FUNCTION + size_t ret = 0; + + while (status() && ret < size) { + const int bytes_written = WiFiClient::write(buffer + ret, size - ret); + if (bytes_written <= 0) { + // connection error + abort(); + return 0; + } + + ret += bytes_written; + } + + return ret; +} + +size_t ClientWrapper::write(uint8_t value) { + TRACE_FUNCTION + return write(&value, 1); +} + +} diff --git a/lib/PicoMQTT/src/PicoMQTT/client_wrapper.h b/lib/PicoMQTT/src/PicoMQTT/client_wrapper.h new file mode 100644 index 00000000..148efec1 --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/client_wrapper.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +namespace PicoMQTT { + +class ClientWrapper: public ::WiFiClient { + public: + ClientWrapper(unsigned long socket_timeout_seconds); + ClientWrapper(const WiFiClient & client, unsigned long socket_timeout_seconds); + ClientWrapper(const ClientWrapper &) = default; + + virtual int peek() override; + virtual int read() override; + virtual int read(uint8_t * buf, size_t size) override; + virtual size_t write(const uint8_t * buffer, size_t size) override; + virtual size_t write(uint8_t value) override final; + +#ifdef ESP32 + // these methods are only available in WiFiClient on ESP8266 + uint8_t status() { return connected(); } + void abort() { stop(); } +#endif + + const unsigned long socket_timeout_millis; + + protected: + int available_wait(unsigned long timeout); +}; + +} diff --git a/lib/PicoMQTT/src/PicoMQTT/config.h b/lib/PicoMQTT/src/PicoMQTT/config.h new file mode 100644 index 00000000..79a6384a --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/config.h @@ -0,0 +1,29 @@ +#pragma once + +#ifndef PICOMQTT_MAX_TOPIC_SIZE +#define PICOMQTT_MAX_TOPIC_SIZE 256 +#endif + +#ifndef PICOMQTT_MAX_MESSAGE_SIZE +#define PICOMQTT_MAX_MESSAGE_SIZE 1024 +#endif + +#ifndef PICOMQTT_MAX_CLIENT_ID_SIZE +/* + * The MQTT standard requires brokers to accept client ids that are + * 1-23 chars long, but allows longer client IDs to be accepted too. + */ +#define PICOMQTT_MAX_CLIENT_ID_SIZE 64 +#endif + +#ifndef PICOMQTT_MAX_USERPASS_SIZE +#define PICOMQTT_MAX_USERPASS_SIZE 256 +#endif + +#ifndef PICOMQTT_OUTGOING_BUFFER_SIZE +#define PICOMQTT_OUTGOING_BUFFER_SIZE 128 +#endif + +// #define PICOMQTT_DEBUG + +// #define PICOMQTT_DEBUG_TRACE_FUNCTIONS diff --git a/lib/PicoMQTT/src/PicoMQTT/connection.cpp b/lib/PicoMQTT/src/PicoMQTT/connection.cpp new file mode 100644 index 00000000..54164933 --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/connection.cpp @@ -0,0 +1,176 @@ +#include "config.h" +#include "connection.h" +#include "debug.h" + +namespace PicoMQTT { + +Connection::Connection(unsigned long keep_alive_seconds, unsigned long socket_timeout_seconds) : + client(socket_timeout_seconds), + keep_alive_millis(keep_alive_seconds * 1000), + last_read(millis()), last_write(millis()) { + TRACE_FUNCTION +} + +Connection::Connection(const ::WiFiClient & client, unsigned long keep_alive_seconds, + unsigned long socket_timeout_seconds) : + client(client, socket_timeout_seconds), + keep_alive_millis(keep_alive_seconds * 1000), + last_read(millis()), last_write(millis()) { + TRACE_FUNCTION +} + +OutgoingPacket Connection::build_packet(Packet::Type type, uint8_t flags, size_t length) { + TRACE_FUNCTION + last_write = millis(); + auto ret = OutgoingPacket(client, type, flags, length); + ret.write_header(); + return ret; +} + +void Connection::on_timeout() { + TRACE_FUNCTION + client.abort(); + on_disconnect(); +} + +void Connection::on_protocol_violation() { + TRACE_FUNCTION + on_disconnect(); +} + +void Connection::on_disconnect() { + TRACE_FUNCTION + client.stop(); +} + +void Connection::disconnect() { + TRACE_FUNCTION + build_packet(Packet::DISCONNECT).send(); + client.stop(); +} + +bool Connection::connected() { + TRACE_FUNCTION + return client.connected(); +} + +void Connection::wait_for_reply(Packet::Type type, std::function handler) { + TRACE_FUNCTION + + const unsigned long start = millis(); + + while (client.connected() && (millis() - start < client.socket_timeout_millis)) { + + IncomingPacket packet(client); + if (!packet) { + break; + } + + last_read = millis(); + + if (packet.get_type() == type) { + handler(packet); + return; + } + + handle_packet(packet); + + } + + if (client.connected()) { + on_timeout(); + } +} + +void Connection::send_ack(Packet::Type ack_type, uint16_t msg_id) { + TRACE_FUNCTION + auto ack = build_packet(ack_type, 0, 2); + ack.write_u16(msg_id); + ack.send(); +} + +void Connection::handle_packet(IncomingPacket & packet) { + TRACE_FUNCTION + + switch (packet.get_type()) { + case Packet::PUBLISH: { + const uint16_t topic_size = packet.read_u16(); + + // const bool dup = (packet.get_flags() >> 3) & 0b1; + const uint8_t qos = (packet.get_flags() >> 1) & 0b11; + // const bool retain = packet.get_flags() & 0b1; + + uint16_t msg_id = 0; + + if (topic_size > PICOMQTT_MAX_TOPIC_SIZE) { + packet.ignore(topic_size); + on_topic_too_long(packet); + if (qos) { + msg_id = packet.read_u16(); + } + } else { + char topic[topic_size + 1]; + if (!packet.read_string(topic, topic_size)) { + // connection error + return; + } + if (qos) { + msg_id = packet.read_u16(); + } + on_message(topic, packet); + } + + if (msg_id) { + send_ack(qos == 1 ? Packet::PUBACK : Packet::PUBREC, msg_id); + } + + break; + }; + + case Packet::PUBREC: + send_ack(Packet::PUBREL, packet.read_u16()); + break; + + case Packet::PUBREL: + send_ack(Packet::PUBCOMP, packet.read_u16()); + break; + + case Packet::PUBCOMP: + // ignore + break; + + case Packet::DISCONNECT: + on_disconnect(); + break; + + default: + on_protocol_violation(); + break; + } +} + +unsigned long Connection::get_millis_since_last_read() const { + TRACE_FUNCTION + return millis() - last_read; +} + +unsigned long Connection::get_millis_since_last_write() const { + TRACE_FUNCTION + return millis() - last_write; +} + +void Connection::loop() { + TRACE_FUNCTION + + // only handle 10 packets max in one go to not starve other connections + for (unsigned int i = 0; (i < 10) && client.available(); ++i) { + IncomingPacket packet(client); + if (!packet.is_valid()) { + return; + } + last_read = millis(); + handle_packet(packet); + } +} + +} diff --git a/lib/PicoMQTT/src/PicoMQTT/connection.h b/lib/PicoMQTT/src/PicoMQTT/connection.h new file mode 100644 index 00000000..ec8c6925 --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/connection.h @@ -0,0 +1,80 @@ +#pragma once + +#include + +#include + +#include "client_wrapper.h" +#include "incoming_packet.h" +#include "outgoing_packet.h" + +namespace PicoMQTT { + +enum ConnectReturnCode : uint8_t { + CRC_ACCEPTED = 0, + CRC_UNACCEPTABLE_PROTOCOL_VERSION = 1, + CRC_IDENTIFIER_REJECTED = 2, + CRC_SERVER_UNAVAILABLE = 3, + CRC_BAD_USERNAME_OR_PASSWORD = 4, + CRC_NOT_AUTHORIZED = 5, + + // internal + CRC_UNDEFINED = 255, +}; + +class Connection { + public: + Connection(unsigned long keep_alive_seconds = 0, unsigned long socket_timeout_seconds = 15); + Connection(const ::WiFiClient & client, unsigned long keep_alive_seconds = 0, + unsigned long socket_timeout_seconds = 15); + Connection(const Connection &) = default; + + virtual ~Connection() {} + + bool connected(); + void disconnect(); + + virtual void loop(); + + protected: + class MessageIdGenerator { + public: + MessageIdGenerator(): value(0) {} + uint16_t generate() { + if (++value == 0) { value = 1; } + return value; + } + + void reset() { value = 0; } + + protected: + uint16_t value; + } message_id_generator; + + OutgoingPacket build_packet(Packet::Type type, uint8_t flags = 0, size_t length = 0); + + void wait_for_reply(Packet::Type type, std::function handler); + + virtual void on_topic_too_long(const IncomingPacket & packet) {} + virtual void on_message(const char * topic, IncomingPacket & packet) {} + + virtual void on_timeout(); + virtual void on_protocol_violation(); + virtual void on_disconnect(); + + ClientWrapper client; + uint16_t keep_alive_millis; + + virtual void handle_packet(IncomingPacket & packet); + + protected: + unsigned long get_millis_since_last_read() const; + unsigned long get_millis_since_last_write() const; + + private: + unsigned long last_read; + unsigned long last_write; + void send_ack(Packet::Type ack_type, uint16_t msg_id); +}; + +} diff --git a/lib/PicoMQTT/src/PicoMQTT/debug.h b/lib/PicoMQTT/src/PicoMQTT/debug.h new file mode 100644 index 00000000..01e39a95 --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/debug.h @@ -0,0 +1,50 @@ +#pragma once + +#include "config.h" + +#ifdef PICOMQTT_DEBUG_TRACE_FUNCTIONS + +#include + +namespace PicoMQTT { + +class FunctionTracer { + public: + FunctionTracer(const char * function_name) : function_name(function_name) { + indent(1); + Serial.print(F("CALL ")); + Serial.println(function_name); + } + + ~FunctionTracer() { + indent(-1); + Serial.print(F("RETURN ")); + Serial.println(function_name); + } + + const char * const function_name; + + protected: + void indent(int delta) { + static int depth = 0; + if (delta < 0) { + depth += delta; + } + for (int i = 0; i < depth; ++i) { + Serial.print(" "); + } + if (delta > 0) { + depth += delta; + } + } +}; + +} + +#define TRACE_FUNCTION FunctionTracer _function_tracer(__PRETTY_FUNCTION__); + +#else + +#define TRACE_FUNCTION + +#endif diff --git a/lib/PicoMQTT/src/PicoMQTT/incoming_packet.cpp b/lib/PicoMQTT/src/PicoMQTT/incoming_packet.cpp new file mode 100644 index 00000000..1b7e60df --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/incoming_packet.cpp @@ -0,0 +1,171 @@ +#include "incoming_packet.h" +#include "debug.h" + +namespace PicoMQTT { + +IncomingPacket::IncomingPacket(Client & client) + : Packet(read_header(client)), client(client) { + TRACE_FUNCTION +} + +IncomingPacket::IncomingPacket(IncomingPacket && other) + : Packet(other), client(other.client) { + TRACE_FUNCTION + other.pos = size; +} + +IncomingPacket::~IncomingPacket() { + TRACE_FUNCTION +#ifdef PICOMQTT_DEBUG + if (pos != size) { + Serial.print(F("IncomingPacket read incorrect number of bytes: ")); + Serial.print(pos); + Serial.print(F("/")); + Serial.println(size); + } +#endif + // read and ignore remaining data + while (get_remaining_size() && (read() >= 0)); +} + +// disabled functions +int IncomingPacket::connect(IPAddress ip, uint16_t port) { + TRACE_FUNCTION; + return 0; +} + +int IncomingPacket::connect(const char * host, uint16_t port) { + TRACE_FUNCTION; + return 0; +} + +size_t IncomingPacket::write(const uint8_t * buffer, size_t size) { + TRACE_FUNCTION + return 0; +} + +size_t IncomingPacket::write(uint8_t value) { + TRACE_FUNCTION + return 0; +} + +void IncomingPacket::flush() { + TRACE_FUNCTION +} + +void IncomingPacket::stop() { + TRACE_FUNCTION +} + + +// extended functions +int IncomingPacket::available() { + TRACE_FUNCTION; + return get_remaining_size(); +} + +int IncomingPacket::peek() { + TRACE_FUNCTION + if (!get_remaining_size()) { +#if PICOMQTT_DEBUG + Serial.println(F("Attempt to peek beyond end of IncomingPacket.")); +#endif + return -1; + } + return client.peek(); +} + +int IncomingPacket::read() { + TRACE_FUNCTION + if (!get_remaining_size()) { +#if PICOMQTT_DEBUG + Serial.println(F("Attempt to read beyond end of IncomingPacket.")); +#endif + return -1; + } + const int ret = client.read(); + if (ret >= 0) { + ++pos; + } + return ret; +} + +int IncomingPacket::read(uint8_t * buf, size_t size) { + TRACE_FUNCTION + const size_t remaining = get_remaining_size(); + const size_t read_size = remaining < size ? remaining : size; +#if PICOMQTT_DEBUG + if (size > remaining) { + Serial.println(F("Attempt to read buf beyond end of IncomingPacket.")); + } +#endif + const int ret = client.read(buf, read_size); + if (ret > 0) { + pos += ret; + } + return ret; +} + +IncomingPacket::operator bool() { + TRACE_FUNCTION + return is_valid() && bool(client); +} + +uint8_t IncomingPacket::connected() { + TRACE_FUNCTION + return is_valid() && client.connected(); +} + +// extra functions +uint8_t IncomingPacket::read_u8() { + TRACE_FUNCTION; + return get_remaining_size() ? read() : 0; +} + +uint16_t IncomingPacket::read_u16() { + TRACE_FUNCTION; + return ((uint16_t) read_u8()) << 8 | ((uint16_t) read_u8()); +} + +bool IncomingPacket::read_string(char * buffer, size_t len) { + if (read((uint8_t *) buffer, len) != (int) len) { + return false; + } + buffer[len] = '\0'; + return true; +} + +void IncomingPacket::ignore(size_t len) { + while (len--) { + read(); + } +} + +Packet IncomingPacket::read_header(Client & client) { + TRACE_FUNCTION + const int head = client.read(); + if (head <= 0) { + return Packet(); + } + + uint32_t size = 0; + for (size_t length_size = 0; ; ++length_size) { + if (length_size >= 5) { + return Packet(); + } + const int digit = client.read(); + if (digit < 0) { + return Packet(); + } + + size |= (digit & 0x7f) << (7 * length_size); + + if (!(digit & 0x80)) { + break; + } + } + + return Packet(head, size); +} + +} diff --git a/lib/PicoMQTT/src/PicoMQTT/incoming_packet.h b/lib/PicoMQTT/src/PicoMQTT/incoming_packet.h new file mode 100644 index 00000000..b89527fa --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/incoming_packet.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include + +#include "packet.h" + +namespace PicoMQTT { + +class IncomingPacket: public Packet, public Client { + public: + IncomingPacket(Client & client); + IncomingPacket(IncomingPacket &&); + + IncomingPacket(const IncomingPacket &) = delete; + const IncomingPacket & operator=(const IncomingPacket &) = delete; + + ~IncomingPacket(); + + virtual int available() override; + virtual int connect(IPAddress ip, uint16_t port) override; + virtual int connect(const char * host, uint16_t port) override; + virtual int peek() override; + virtual int read() override; + virtual int read(uint8_t * buf, size_t size) override; + // This operator is not marked explicit in the Client base class. Still, we're marking it explicit here + // to block implicit conversions to integer types. + virtual explicit operator bool() override; + virtual size_t write(const uint8_t * buffer, size_t size) override; + virtual size_t write(uint8_t value) override final; + virtual uint8_t connected() override; + virtual void flush() override; + virtual void stop() override; + + uint8_t read_u8(); + uint16_t read_u16(); + bool read_string(char * buffer, size_t len); + void ignore(size_t len); + + protected: + static Packet read_header(Client & client); + + Client & client; +}; + +} diff --git a/lib/PicoMQTT/src/PicoMQTT/outgoing_packet.cpp b/lib/PicoMQTT/src/PicoMQTT/outgoing_packet.cpp new file mode 100644 index 00000000..333c3522 --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/outgoing_packet.cpp @@ -0,0 +1,224 @@ +#include +#include + +#include "debug.h" +#include "outgoing_packet.h" + +namespace PicoMQTT { + +OutgoingPacket::OutgoingPacket(Print & print, Packet::Type type, uint8_t flags, size_t payload_size) + : Packet(type, flags, payload_size), print(print), +#ifndef PICOMQTT_UNBUFFERED + buffer_position(0), +#endif + state(State::ok) { + TRACE_FUNCTION +} + +OutgoingPacket::OutgoingPacket(OutgoingPacket && other) + : OutgoingPacket(other) { + TRACE_FUNCTION + other.state = State::dead; +} + +OutgoingPacket::~OutgoingPacket() { + TRACE_FUNCTION +#ifdef PICOMQTT_DEBUG +#ifndef PICOMQTT_UNBUFFERED + if (buffer_position) { + Serial.printf("OutgoingPacket has unsent data in the buffer (pos=%u)\n", buffer_position); + } +#endif + switch (state) { + case State::ok: + Serial.println(F("Unsent OutgoingPacket")); + break; + case State::sent: + if (pos != size) { + Serial.print(F("OutgoingPacket sent incorrect number of bytes: ")); + Serial.print(pos); + Serial.print(F("/")); + Serial.println(size); + } + break; + default: + break; + } +#endif +} + +size_t OutgoingPacket::write_from_client(::Client & client, size_t length) { + TRACE_FUNCTION + size_t written = 0; +#ifndef PICOMQTT_UNBUFFERED + while (written < length) { + const size_t remaining = length - written; + const size_t remaining_buffer_space = PICOMQTT_OUTGOING_BUFFER_SIZE - buffer_position; + const size_t chunk_size = remaining < remaining_buffer_space ? remaining : remaining_buffer_space; + + const int read_size = client.read(buffer + buffer_position, chunk_size); + if (read_size <= 0) { + break; + } + + buffer_position += (size_t) read_size; + written += (size_t) read_size; + + if (buffer_position >= PICOMQTT_OUTGOING_BUFFER_SIZE) { + flush(); + } + } +#else + uint8_t buffer[128] __attribute__((aligned(4))); + while (written < length) { + const size_t remain = length - written; + const size_t chunk_size = sizeof(buffer) < remain ? sizeof(buffer) : remain; + const int read_size = client.read(buffer, chunk_size); + if (read_size <= 0) { + break; + } + const size_t write_size = print.write(buffer, read_size); + written += write_size; + if (!write_size) { + break; + } + } +#endif + pos += written; + return written; +} + +size_t OutgoingPacket::write_zero(size_t length) { + TRACE_FUNCTION + for (size_t written = 0; written < length; ++written) { + write_u8('0'); + } + return length; +} + +#ifndef PICOMQTT_UNBUFFERED +size_t OutgoingPacket::write(const void * data, size_t remaining, void * (*memcpy_fn)(void *, const void *, size_t n)) { + TRACE_FUNCTION + + const char * src = (const char *) data; + + while (remaining) { + const size_t remaining_buffer_space = PICOMQTT_OUTGOING_BUFFER_SIZE - buffer_position; + const size_t chunk_size = remaining < remaining_buffer_space ? remaining : remaining_buffer_space; + + memcpy_fn(buffer + buffer_position, src, chunk_size); + + buffer_position += chunk_size; + src += chunk_size; + remaining -= chunk_size; + + if (buffer_position >= PICOMQTT_OUTGOING_BUFFER_SIZE) { + flush(); + } + } + + const size_t written = src - (const char *) data; + pos += written; + return written; +} +#endif + +size_t OutgoingPacket::write(const uint8_t * data, size_t length) { + TRACE_FUNCTION +#ifndef PICOMQTT_UNBUFFERED + return write(data, length, memcpy); +#else + const size_t written = print.write(data, length); + pos += written; + return written; +#endif +} + +size_t OutgoingPacket::write_P(PGM_P data, size_t length) { + TRACE_FUNCTION +#ifndef PICOMQTT_UNBUFFERED + return write(data, length, memcpy_P); +#else + // here we will need a buffer + uint8_t buffer[128] __attribute__((aligned(4))); + size_t written = 0; + while (written < length) { + const size_t remain = length - written; + const size_t chunk_size = sizeof(buffer) < remain ? sizeof(buffer) : remain; + memcpy_P(buffer, data, chunk_size); + const size_t write_size = print.write(buffer, chunk_size); + written += write_size; + data += write_size; + if (!write_size) { + break; + } + } + pos += written; + return written; +#endif +} + +size_t OutgoingPacket::write_u8(uint8_t c) { + TRACE_FUNCTION + return write(&c, 1); +} + +size_t OutgoingPacket::write_u16(uint16_t value) { + TRACE_FUNCTION + return write_u8(value >> 8) + write_u8(value & 0xff); +} + +size_t OutgoingPacket::write_string(const char * string, uint16_t size) { + TRACE_FUNCTION + return write_u16(size) + write((const uint8_t *) string, size); +} + +size_t OutgoingPacket::write_packet_length(size_t length) { + TRACE_FUNCTION + size_t ret = 0; + do { + const uint8_t digit = length & 127; // digit := length % 128 + length >>= 7; // length := length / 128 + ret += write_u8(digit | (length ? 0x80 : 0)); + } while (length); + return ret; +} + +size_t OutgoingPacket::write_header() { + TRACE_FUNCTION + const size_t ret = write_u8(head) + write_packet_length(size); + // we've just written the header, payload starts now + pos = 0; + return ret; +} + +void OutgoingPacket::flush() { + TRACE_FUNCTION +#ifndef PICOMQTT_UNBUFFERED + print.write(buffer, buffer_position); + buffer_position = 0; +#endif +} + +bool OutgoingPacket::send() { + TRACE_FUNCTION + const size_t remaining_size = get_remaining_size(); + if (remaining_size) { +#ifdef PICOMQTT_DEBUG + Serial.printf("OutgoingPacket sent called on incomplete payload (%u / %u), filling with zeros.\n", pos, size); +#endif + write_zero(remaining_size); + } + flush(); + switch (state) { + case State::ok: + // print.flush(); + state = State::sent; + case State::sent: + return true; + default: + return false; + } +} + +} diff --git a/lib/PicoMQTT/src/PicoMQTT/outgoing_packet.h b/lib/PicoMQTT/src/PicoMQTT/outgoing_packet.h new file mode 100644 index 00000000..f3776d5f --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/outgoing_packet.h @@ -0,0 +1,64 @@ +#pragma once + +// #define MQTT_OUTGOING_PACKET_DEBUG + +#include + +#include "config.h" +#include "packet.h" + +class Print; +class Client; + +#if PICOMQTT_OUTGOING_BUFFER_SIZE == 0 +#define PICOMQTT_UNBUFFERED +#endif + +namespace PicoMQTT { + +class OutgoingPacket: public Packet, public Print { + public: + OutgoingPacket(Print & print, Type type, uint8_t flags, size_t payload_size); + virtual ~OutgoingPacket(); + + const OutgoingPacket & operator=(const OutgoingPacket &) = delete; + OutgoingPacket(OutgoingPacket && other); + + virtual size_t write(const uint8_t * data, size_t length) override; + virtual size_t write(uint8_t value) override final { return write(&value, 1); } + + size_t write_P(PGM_P data, size_t length); + size_t write_u8(uint8_t value); + size_t write_u16(uint16_t value); + size_t write_string(const char * string, uint16_t size); + size_t write_header(); + + size_t write_from_client(::Client & c, size_t length); + size_t write_zero(size_t count); + + virtual void flush() override; + virtual bool send(); + + protected: + OutgoingPacket(const OutgoingPacket &) = default; + + size_t write(const void * data, size_t length, void * (*memcpy_fn)(void *, const void *, size_t n)); + size_t write_packet_length(size_t length); + + Print & print; + +#ifndef PICOMQTT_UNBUFFERED + uint8_t buffer[PICOMQTT_OUTGOING_BUFFER_SIZE] __attribute__((aligned(4))); + + size_t buffer_position; +#endif + + enum class State { + ok, + sent, + error, + dead, + } state; +}; + +} diff --git a/lib/PicoMQTT/src/PicoMQTT/packet.h b/lib/PicoMQTT/src/PicoMQTT/packet.h new file mode 100644 index 00000000..5efd6a7e --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/packet.h @@ -0,0 +1,49 @@ +#pragma once + +#include + +namespace PicoMQTT { + +class Packet { + public: + enum Type : uint8_t { + ERROR = 0, + CONNECT = 1 << 4, // Client request to connect to Server + CONNACK = 2 << 4, // Connect Acknowledgment + PUBLISH = 3 << 4, // Publish message + PUBACK = 4 << 4, // Publish Acknowledgment + PUBREC = 5 << 4, // Publish Received (assured delivery part 1) + PUBREL = 6 << 4, // Publish Release (assured delivery part 2) + PUBCOMP = 7 << 4, // Publish Complete (assured delivery part 3) + SUBSCRIBE = 8 << 4, // Client Subscribe request + SUBACK = 9 << 4, // Subscribe Acknowledgment + UNSUBSCRIBE = 10 << 4, // Client Unsubscribe request + UNSUBACK = 11 << 4, // Unsubscribe Acknowledgment + PINGREQ = 12 << 4, // PING Request + PINGRESP = 13 << 4, // PING Response + DISCONNECT = 14 << 4, // Client is Disconnecting + }; + + Packet(uint8_t head, size_t size) + : head(head), size(size), pos(0) {} + + Packet(Type type = ERROR, const uint8_t flags = 0, size_t size = 0) + : Packet((uint8_t) type | (flags & 0xf), size) { + } + + virtual ~Packet() {} + + Type get_type() const { return Type(head & 0xf0); } + uint8_t get_flags() const { return head & 0x0f; } + + bool is_valid() { return get_type() != ERROR; } + size_t get_remaining_size() const { return pos < size ? size - pos : 0; } + + const uint8_t head; + const size_t size; + + protected: + size_t pos; +}; + +} diff --git a/lib/PicoMQTT/src/PicoMQTT/pico_interface.h b/lib/PicoMQTT/src/PicoMQTT/pico_interface.h new file mode 100644 index 00000000..2f823b6f --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/pico_interface.h @@ -0,0 +1,13 @@ +#pragma once + +namespace PicoMQTT { + +class PicoMQTTInterface { + public: + virtual ~PicoMQTTInterface() {} + virtual void begin() {} + virtual void stop() {} + virtual void loop() {} +}; + +} diff --git a/lib/PicoMQTT/src/PicoMQTT/print_mux.cpp b/lib/PicoMQTT/src/PicoMQTT/print_mux.cpp new file mode 100644 index 00000000..bbe970f6 --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/print_mux.cpp @@ -0,0 +1,29 @@ +#include "print_mux.h" +#include "debug.h" + +namespace PicoMQTT { + +size_t PrintMux::write(uint8_t value) { + TRACE_FUNCTION + for (auto print_ptr : prints) { + print_ptr->write(value); + } + return 1; +} + +size_t PrintMux::write(const uint8_t * buffer, size_t size) { + TRACE_FUNCTION + for (auto print_ptr : prints) { + print_ptr->write(buffer, size); + } + return size; +} + +void PrintMux::flush() { + TRACE_FUNCTION + for (auto print_ptr : prints) { + print_ptr->flush(); + } +} + +} diff --git a/lib/PicoMQTT/src/PicoMQTT/print_mux.h b/lib/PicoMQTT/src/PicoMQTT/print_mux.h new file mode 100644 index 00000000..b1dc688d --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/print_mux.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include + +namespace PicoMQTT { + +class PrintMux: public ::Print { + public: + PrintMux() {} + + PrintMux(Print & print) : prints({&print}) {} + + void add(Print & print) { + prints.push_back(&print); + } + + virtual size_t write(uint8_t) override; + virtual size_t write(const uint8_t * buffer, size_t size) override; + virtual void flush(); + + size_t size() const { return prints.size(); } + + protected: + std::vector prints; +}; + +} diff --git a/lib/PicoMQTT/src/PicoMQTT/publisher.cpp b/lib/PicoMQTT/src/PicoMQTT/publisher.cpp new file mode 100644 index 00000000..506afd70 --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/publisher.cpp @@ -0,0 +1,56 @@ +#include "publisher.h" +#include "debug.h" + +namespace PicoMQTT { + +Publisher::Publish::Publish(Publisher & publisher, const PrintMux & print, + uint8_t flags, size_t total_size, + const char * topic, size_t topic_size, + uint16_t message_id) + : + OutgoingPacket(this->print, Packet::PUBLISH, flags, total_size), + qos((flags >> 1) & 0b11), + message_id(message_id), + print(print), + publisher(publisher) { + TRACE_FUNCTION + + OutgoingPacket::write_header(); + write_string(topic, topic_size); + if (qos) { + write_u16(message_id); + } +} + +Publisher::Publish::Publish(Publisher & publisher, const PrintMux & print, + const char * topic, size_t topic_size, size_t payload_size, + uint8_t qos, bool retain, bool dup, uint16_t message_id) + : Publish( + publisher, print, + (dup ? 0b1000 : 0) | ((qos & 0b11) << 1) | (retain ? 1 : 0), // flags + 2 + topic_size + (qos ? 2 : 0) + payload_size, // total size + topic, topic_size, // topic + message_id) { + TRACE_FUNCTION +} + +Publisher::Publish::Publish(Publisher & publisher, const PrintMux & print, + const char * topic, size_t payload_size, + uint8_t qos, bool retain, bool dup, uint16_t message_id) + : Publish( + publisher, print, + topic, strlen(topic), payload_size, + qos, retain, dup, message_id) { + TRACE_FUNCTION +} + +Publisher::Publish::~Publish() { + TRACE_FUNCTION +} + +bool Publisher::Publish::send() { + TRACE_FUNCTION + return OutgoingPacket::send() && publisher.on_publish_complete(*this); +} + +} diff --git a/lib/PicoMQTT/src/PicoMQTT/publisher.h b/lib/PicoMQTT/src/PicoMQTT/publisher.h new file mode 100644 index 00000000..56e3a790 --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/publisher.h @@ -0,0 +1,90 @@ +#pragma once + +#include + +#include + +#include "debug.h" +#include "outgoing_packet.h" +#include "print_mux.h" + +namespace PicoMQTT { + +class Publisher { + public: + class Publish: public OutgoingPacket { + private: + Publish(Publisher & publisher, const PrintMux & print, + uint8_t flags, size_t total_size, + const char * topic, size_t topic_size, + uint16_t message_id); + + public: + Publish(Publisher & publisher, const PrintMux & print, + const char * topic, size_t topic_size, size_t payload_size, + uint8_t qos = 0, bool retain = false, bool dup = false, uint16_t message_id = 0); + + Publish(Publisher & publisher, const PrintMux & print, + const char * topic, size_t payload_size, + uint8_t qos = 0, bool retain = false, bool dup = false, uint16_t message_id = 0); + + ~Publish(); + + virtual bool send() override; + + const uint8_t qos; + const uint16_t message_id; + PrintMux print; + Publisher & publisher; + }; + + virtual Publish begin_publish(const char * topic, const size_t payload_size, + uint8_t qos = 0, bool retain = false, uint16_t message_id = 0) = 0; + + Publish begin_publish(const String & topic, const size_t payload_size, + uint8_t qos = 0, bool retain = false, uint16_t message_id = 0) { + return begin_publish(topic.c_str(), payload_size, qos, retain, message_id); + } + + template + bool publish(TopicStringType topic, const void * payload, const size_t payload_size, + uint8_t qos = 0, bool retain = false, uint16_t message_id = 0) { + TRACE_FUNCTION + auto packet = begin_publish(get_c_str(topic), payload_size, qos, retain, message_id); + packet.write((const uint8_t *) payload, payload_size); + return packet.send(); + } + + template + bool publish_P(TopicStringType topic, PGM_P payload, const size_t payload_size, + uint8_t qos = 0, bool retain = false, uint16_t message_id = 0) { + TRACE_FUNCTION; + auto packet = begin_publish(get_c_str(topic), payload_size, qos, retain, message_id); + packet.write_P(payload, payload_size); + return packet.send(); + } + + template + bool publish(TopicStringType topic, PayloadStringType payload, + uint8_t qos = 0, bool retain = false, uint16_t message_id = 0) { + return publish(topic, (const void *) get_c_str(payload), get_c_str_len(payload), + qos, retain, message_id); + } + + template + bool publish_P(TopicStringType topic, PGM_P payload, + uint8_t qos = 0, bool retain = false, uint16_t message_id = 0) { + return publish_P(topic, payload, strlen_P(payload), + qos, retain, message_id); + } + + protected: + virtual bool on_publish_complete(const Publish & publish) { return true; } + + static const char * get_c_str(const char * string) { return string; } + static const char * get_c_str(const String & string) { return string.c_str(); } + static size_t get_c_str_len(const char * string) { return strlen(string); } + static size_t get_c_str_len(const String & string) { return string.length(); } +}; + +} diff --git a/lib/PicoMQTT/src/PicoMQTT/server.cpp b/lib/PicoMQTT/src/PicoMQTT/server.cpp new file mode 100644 index 00000000..c3c82f64 --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/server.cpp @@ -0,0 +1,365 @@ +#include "config.h" +#include "debug.h" +#include "server.h" + +namespace PicoMQTT { + +BasicServer::Client::Client(const BasicServer::Client & other) + : Connection(other.client, 0), + server(other.server), + client_id(other.client_id) { + TRACE_FUNCTION +} + +BasicServer::Client::Client(BasicServer & server, const WiFiClient & client) + : Connection(client, 0, server.socket_timeout_seconds), server(server), client_id("") { + TRACE_FUNCTION + wait_for_reply(Packet::CONNECT, [this](IncomingPacket & packet) { + TRACE_FUNCTION + + auto connack = [this](ConnectReturnCode crc) { + TRACE_FUNCTION + auto connack = build_packet(Packet::CONNACK, 0, 2); + connack.write_u8(0); /* session present always set to zero */ + connack.write_u8(crc); + connack.send(); + if (crc != CRC_ACCEPTED) { + this->client.stop(); + } + }; + + { + // MQTT protocol identifier + char buf[4]; + + if (packet.read_u16() != 4) { + on_protocol_violation(); + return; + } + + packet.read((uint8_t *) buf, 4); + + if (memcmp(buf, "MQTT", 4) != 0) { + on_protocol_violation(); + return; + } + } + + const uint8_t protocol_level = packet.read_u8(); + if (protocol_level != 4) { + on_protocol_violation(); + return; + } + + const uint8_t connect_flags = packet.read_u8(); + const bool has_user = connect_flags & (1 << 7); + const bool has_pass = connect_flags & (1 << 6); + const bool will_retain = connect_flags & (1 << 5); + const uint8_t will_qos = (connect_flags >> 3) & 0b11; + const bool has_will = connect_flags & (1 << 2); + /* const bool clean_session = connect_flags & (1 << 1); */ + + if ((has_pass && !has_user) + || (will_qos > 2) + || (!has_will && ((will_qos > 0) || will_retain))) { + on_protocol_violation(); + return; + } + + const uint16_t keep_alive_seconds = packet.read_u16(); + keep_alive_millis = keep_alive_seconds ? (keep_alive_seconds + this->server.keep_alive_tolerance_seconds) * 1000 : 0; + + { + const size_t client_id_size = packet.read_u16(); + if (client_id_size > PICOMQTT_MAX_CLIENT_ID_SIZE) { + connack(CRC_IDENTIFIER_REJECTED); + return; + } + + char client_id_buffer[client_id_size + 1]; + packet.read_string(client_id_buffer, client_id_size); + client_id = client_id_buffer; + } + + if (client_id.isEmpty()) { + client_id = String((unsigned int)(this), HEX); + } + + if (has_will) { + packet.ignore(packet.read_u16()); // will topic + packet.ignore(packet.read_u16()); // will payload + } + + // read username + const size_t user_size = has_user ? packet.read_u16() : 0; + if (user_size > PICOMQTT_MAX_USERPASS_SIZE) { + connack(CRC_BAD_USERNAME_OR_PASSWORD); + return; + } + char user[user_size + 1]; + if (user_size && !packet.read_string(user, user_size)) { + on_timeout(); + return; + } + + // read password + const size_t pass_size = has_pass ? packet.read_u16() : 0; + if (pass_size > PICOMQTT_MAX_USERPASS_SIZE) { + connack(CRC_BAD_USERNAME_OR_PASSWORD); + return; + } + char pass[pass_size + 1]; + if (pass_size && !packet.read_string(pass, pass_size)) { + on_timeout(); + return; + } + + const auto connect_return_code = this->server.auth( + client_id.c_str(), + has_user ? user : nullptr, has_pass ? pass : nullptr); + + connack(connect_return_code); + }); +} + +void BasicServer::Client::on_message(const char * topic, IncomingPacket & packet) { + TRACE_FUNCTION + + const size_t payload_size = packet.get_remaining_size(); + auto publish = server.begin_publish(topic, payload_size); + + // Always notify the server about the message + { + IncomingPublish incoming_publish(packet, publish); + server.on_message(topic, incoming_publish); + } + + publish.send(); +} + +void BasicServer::Client::on_subscribe(IncomingPacket & subscribe) { + TRACE_FUNCTION + const uint16_t message_id = subscribe.read_u16(); + + if ((subscribe.get_flags() != 0b0010) || !message_id) { + on_protocol_violation(); + return; + } + + std::list suback_codes; + + while (subscribe.get_remaining_size()) { + const size_t topic_size = subscribe.read_u16(); + if (topic_size > PICOMQTT_MAX_TOPIC_SIZE) { + subscribe.ignore(topic_size); + subscribe.read_u8(); + suback_codes.push_back(0x80); + } else { + char topic[topic_size + 1]; + if (!subscribe.read_string(topic, topic_size)) { + // connection error + return; + } + uint8_t qos = subscribe.read_u8(); + if (qos > 2) { + on_protocol_violation(); + return; + } + this->subscribe(topic); + server.on_subscribe(client_id.c_str(), topic); + suback_codes.push_back(0); + } + } + + auto suback = build_packet(Packet::SUBACK, 0, 2 + suback_codes.size()); + suback.write_u16(message_id); + for (uint8_t code : suback_codes) { + suback.write_u8(code); + } + suback.send(); +} + +void BasicServer::Client::on_unsubscribe(IncomingPacket & unsubscribe) { + TRACE_FUNCTION + const uint16_t message_id = unsubscribe.read_u16(); + + if ((unsubscribe.get_flags() != 0b0010) || !message_id) { + on_protocol_violation(); + return; + } + + while (unsubscribe.get_remaining_size()) { + const size_t topic_size = unsubscribe.read_u16(); + if (topic_size > PICOMQTT_MAX_TOPIC_SIZE) { + unsubscribe.ignore(topic_size); + } else { + char topic[topic_size + 1]; + if (!unsubscribe.read_string(topic, topic_size)) { + // connection error + return; + } + server.on_unsubscribe(client_id.c_str(), topic); + this->unsubscribe(topic); + } + } + + auto unsuback = build_packet(Packet::UNSUBACK, 0, 2); + unsuback.write_u16(message_id); + unsuback.send(); +} + +const char * BasicServer::Client::get_subscription_pattern(BasicServer::Client::SubscriptionId id) const { + for (const auto & pattern : subscriptions) + if (pattern.id == id) { + return pattern.c_str(); + } + return nullptr; +} + +Server::SubscriptionId BasicServer::Client::get_subscription(const char * topic) const { + TRACE_FUNCTION + for (const auto & pattern : subscriptions) + if (topic_matches(pattern.c_str(), topic)) { + return pattern.id; + } + return 0; +} + +BasicServer::Client::SubscriptionId BasicServer::Client::subscribe(const String & topic_filter) { + TRACE_FUNCTION + const Subscription subscription(topic_filter.c_str()); + subscriptions.insert(subscription); + return subscription.id; +} + +void BasicServer::Client::unsubscribe(const String & topic_filter) { + TRACE_FUNCTION + subscriptions.erase(topic_filter.c_str()); +} + +void BasicServer::Client::handle_packet(IncomingPacket & packet) { + TRACE_FUNCTION + + switch (packet.get_type()) { + case Packet::PINGREQ: + build_packet(Packet::PINGRESP).send(); + return; + + case Packet::SUBSCRIBE: + on_subscribe(packet); + return; + + case Packet::UNSUBSCRIBE: + on_unsubscribe(packet); + return; + + default: + Connection::handle_packet(packet); + return; + } +} + +void BasicServer::Client::loop() { + TRACE_FUNCTION + if (keep_alive_millis && (get_millis_since_last_read() > keep_alive_millis)) { + // ping timeout + on_timeout(); + return; + } + + Connection::loop(); +} + +BasicServer::IncomingPublish::IncomingPublish(IncomingPacket & packet, Publish & publish) + : IncomingPacket(std::move(packet)), publish(publish) { + TRACE_FUNCTION +} + +BasicServer::IncomingPublish::~IncomingPublish() { + TRACE_FUNCTION + pos += publish.write_from_client(client, get_remaining_size()); +} + +int BasicServer::IncomingPublish::read(uint8_t * buf, size_t size) { + TRACE_FUNCTION + const int ret = IncomingPacket::read(buf, size); + if (ret > 0) { + publish.write(buf, ret); + } + return ret; +} + +int BasicServer::IncomingPublish::read() { + TRACE_FUNCTION + const int ret = IncomingPacket::read(); + if (ret >= 0) { + publish.write(ret); + } + return ret; +} + +BasicServer::BasicServer(uint16_t port, unsigned long keep_alive_tolerance_seconds, + unsigned long socket_timeout_seconds) + : server(port), keep_alive_tolerance_seconds(keep_alive_tolerance_seconds), + socket_timeout_seconds(socket_timeout_seconds) { + TRACE_FUNCTION +} + +void BasicServer::begin() { + TRACE_FUNCTION + server.begin(); +} + +void BasicServer::stop() { + TRACE_FUNCTION + server.stop(); + clients.clear(); +} + +void BasicServer::loop() { + TRACE_FUNCTION + + while (server.hasClient()) { + auto client = Client(*this, server.accept()); + clients.push_back(client); + on_connected(client.get_client_id()); + } + + for (auto it = clients.begin(); it != clients.end();) { + it->loop(); + + if (!it->connected()) { + on_disconnected(it->get_client_id()); + clients.erase(it++); + } else { + ++it; + } + } +} + +PrintMux BasicServer::get_subscribed(const char * topic) { + TRACE_FUNCTION + PrintMux ret; + for (auto & client : clients) { + if (client.get_subscription(topic)) { + ret.add(client.get_print()); + } + } + return ret; +} + +Publisher::Publish BasicServer::begin_publish(const char * topic, const size_t payload_size, + uint8_t, bool, uint16_t) { + TRACE_FUNCTION + return Publish(*this, get_subscribed(topic), topic, payload_size); +} + +void BasicServer::on_message(const char * topic, IncomingPacket & packet) { +} + +void Server::on_message(const char * topic, IncomingPacket & packet) { + TRACE_FUNCTION + fire_message_callbacks(topic, packet); +} + +} diff --git a/lib/PicoMQTT/src/PicoMQTT/server.h b/lib/PicoMQTT/src/PicoMQTT/server.h new file mode 100644 index 00000000..35516e5e --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/server.h @@ -0,0 +1,105 @@ +#pragma once + +#include +#include + +#include + +#if defined(ESP32) +#include +#elif defined(ESP8266) +#include +#else +#error "This board is not supported." +#endif + +#include "debug.h" +#include "incoming_packet.h" +#include "connection.h" +#include "publisher.h" +#include "subscriber.h" +#include "pico_interface.h" + +namespace PicoMQTT { + +class BasicServer: public PicoMQTTInterface, public Publisher { + public: + class Client: public Connection, public Subscriber { + public: + Client(BasicServer & server, const WiFiClient & client); + Client(const Client &); + + void on_message(const char * topic, IncomingPacket & packet) override; + + Print & get_print() { return client; } + const char * get_client_id() const { return client_id.c_str(); } + + virtual void loop() override; + + virtual const char * get_subscription_pattern(SubscriptionId id) const override; + virtual SubscriptionId get_subscription(const char * topic) const override; + virtual SubscriptionId subscribe(const String & topic_filter) override; + virtual void unsubscribe(const String & topic_filter) override; + + protected: + BasicServer & server; + String client_id; + std::set subscriptions; + + virtual void on_subscribe(IncomingPacket & packet); + virtual void on_unsubscribe(IncomingPacket & packet); + + virtual void handle_packet(IncomingPacket & packet) override; + }; + + class IncomingPublish: public IncomingPacket { + public: + IncomingPublish(IncomingPacket & packet, Publish & publish); + IncomingPublish(const IncomingPublish &) = delete; + ~IncomingPublish(); + + virtual int read(uint8_t * buf, size_t size) override; + virtual int read() override; + + protected: + Publish & publish; + }; + + BasicServer(uint16_t port = 1883, unsigned long keep_alive_tolerance_seconds = 10, + unsigned long socket_timeout_seconds = 5); + + void begin() override; + void stop() override; + void loop() override; + + using Publisher::begin_publish; + virtual Publish begin_publish(const char * topic, const size_t payload_size, + uint8_t qos = 0, bool retain = false, uint16_t message_id = 0) override; + + protected: + virtual void on_message(const char * topic, IncomingPacket & packet); + virtual ConnectReturnCode auth(const char * client_id, const char * username, const char * password) { return CRC_ACCEPTED; } + + virtual void on_connected(const char * client_id) {} + virtual void on_disconnected(const char * client_id) {} + + virtual void on_subscribe(const char * client_id, const char * topic) {} + virtual void on_unsubscribe(const char * client_id, const char * topic) {} + + virtual PrintMux get_subscribed(const char * topic); + + WiFiServer server; + std::list clients; + + const unsigned long keep_alive_tolerance_seconds; + const unsigned long socket_timeout_seconds; + +}; + +class Server: public BasicServer, public SubscribedMessageListener { + public: + using BasicServer::BasicServer; + virtual void on_message(const char * topic, IncomingPacket & packet) override; +}; + +} diff --git a/lib/PicoMQTT/src/PicoMQTT/subscriber.cpp b/lib/PicoMQTT/src/PicoMQTT/subscriber.cpp new file mode 100644 index 00000000..971195ea --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/subscriber.cpp @@ -0,0 +1,174 @@ +#include "subscriber.h" +#include "incoming_packet.h" +#include "debug.h" + +namespace PicoMQTT { + +String Subscriber::get_topic_element(const char * topic, size_t index) { + + while (index && topic[0]) { + if (topic++[0] == '/') { + --index; + } + } + + if (!topic[0]) { + return ""; + } + + const char * end = topic; + while (*end && *end != '/') { + ++end; + } + + String ret; + ret.concat(topic, end - topic); + return ret; +} + +String Subscriber::get_topic_element(const String & topic, size_t index) { + TRACE_FUNCTION + return get_topic_element(topic.c_str(), index); +} + +bool Subscriber::topic_matches(const char * p, const char * t) { + TRACE_FUNCTION + // TODO: Special handling of the $ prefix + while (true) { + switch (*p) { + case '\0': + // end of pattern reached + // TODO: check for '/#' suffix + return (*t == '\0'); + + case '#': + // multilevel wildcard + if (*t == '\0') { + return false; + } + return true; + + case '+': + // single level wildcard + while (*t && *t != '/') { + ++t; + } + ++p; + break; + + default: + // regular match + if (*p != *t) { + if (*t == '\0') + { + if (*p == '/') + { + ++p; + if (*p == '#') + { + ++p; + if (*p == '\0') + return true; + } + } + } + return false; + } + ++p; + ++t; + } + } +} + +const char * SubscribedMessageListener::get_subscription_pattern(SubscriptionId id) const { + TRACE_FUNCTION + for (const auto & kv : subscriptions) { + if (kv.first.id == id) { + return kv.first.c_str(); + } + } + return nullptr; +} + +Subscriber::SubscriptionId SubscribedMessageListener::get_subscription(const char * topic) const { + TRACE_FUNCTION + for (const auto & kv : subscriptions) { + if (topic_matches(kv.first.c_str(), topic)) { + return kv.first.id; + } + } + return 0; +} + +Subscriber::SubscriptionId SubscribedMessageListener::subscribe(const String & topic_filter) { + TRACE_FUNCTION + return subscribe(topic_filter, [this](const char * topic, IncomingPacket & packet) { on_extra_message(topic, packet); }); +} + +Subscriber::SubscriptionId SubscribedMessageListener::subscribe(const String & topic_filter, MessageCallback callback) { + TRACE_FUNCTION + unsubscribe(topic_filter); + auto pair = subscriptions.emplace(std::make_pair(Subscription(topic_filter), callback)); + return pair.first->first.id; +} + +void SubscribedMessageListener::unsubscribe(const String & topic_filter) { + TRACE_FUNCTION + subscriptions.erase(topic_filter); +} + +void SubscribedMessageListener::fire_message_callbacks(const char * topic, IncomingPacket & packet) { + TRACE_FUNCTION + for (const auto & kv : subscriptions) { + if (topic_matches(kv.first.c_str(), topic)) { + kv.second((char *) topic, packet); + return; + } + } + on_extra_message(topic, packet); +} + +Subscriber::SubscriptionId SubscribedMessageListener::subscribe(const String & topic_filter, + std::function callback, size_t max_size) { + TRACE_FUNCTION + return subscribe(topic_filter, [this, callback, max_size](char * topic, IncomingPacket & packet) { + const size_t payload_size = packet.get_remaining_size(); + if (payload_size >= max_size) { + on_message_too_big(topic, packet); + return; + } + char payload[payload_size + 1]; + if (packet.read((uint8_t *) payload, payload_size) != (int) payload_size) { + // connection error, ignore + return; + } + payload[payload_size] = '\0'; + callback(topic, payload, payload_size); + }); +} + +Subscriber::SubscriptionId SubscribedMessageListener::subscribe(const String & topic_filter, + std::function callback, size_t max_size) { + TRACE_FUNCTION + return subscribe(topic_filter, [callback](char * topic, void * payload, size_t payload_size) { + callback(topic, (char *) payload); + }); +} + +Subscriber::SubscriptionId SubscribedMessageListener::subscribe(const String & topic_filter, + std::function callback, size_t max_size) { + TRACE_FUNCTION + return subscribe(topic_filter, [callback](char * topic, void * payload, size_t payload_size) { + callback((char *) payload); + }); +} + +Subscriber::SubscriptionId SubscribedMessageListener::subscribe(const String & topic_filter, + std::function callback, size_t max_size) { + TRACE_FUNCTION + return subscribe(topic_filter, [callback](char * topic, void * payload, size_t payload_size) { + callback(payload, payload_size); + }); +} + +}; diff --git a/lib/PicoMQTT/src/PicoMQTT/subscriber.h b/lib/PicoMQTT/src/PicoMQTT/subscriber.h new file mode 100644 index 00000000..1fe5ace5 --- /dev/null +++ b/lib/PicoMQTT/src/PicoMQTT/subscriber.h @@ -0,0 +1,73 @@ +#pragma once + +#include +#include + +#include + +#include "autoid.h" +#include "config.h" + +namespace PicoMQTT { + +class IncomingPacket; + +class Subscriber { + public: + typedef AutoId::Id SubscriptionId; + + static bool topic_matches(const char * topic_filter, const char * topic); + static String get_topic_element(const char * topic, size_t index); + static String get_topic_element(const String & topic, size_t index); + + virtual const char * get_subscription_pattern(SubscriptionId id) const = 0; + virtual SubscriptionId get_subscription(const char * topic) const = 0; + + virtual SubscriptionId subscribe(const String & topic_filter) = 0; + + virtual void unsubscribe(const String & topic_filter) = 0; + void unsubscribe(SubscriptionId id) { unsubscribe(get_subscription_pattern(id)); } + + protected: + class Subscription: public String, public AutoId { + public: + using String::String; + Subscription(const String & str): Subscription(str.c_str()) {} + }; + +}; + +class SubscribedMessageListener: public Subscriber { + public: + // NOTE: None of the callback functions use const arguments for wider compatibility. It's still OK (and + // recommended) to use callbacks which take const arguments. Similarly with Strings. + typedef std::function MessageCallback; + + virtual const char * get_subscription_pattern(SubscriptionId id) const override; + virtual SubscriptionId get_subscription(const char * topic) const override; + + virtual SubscriptionId subscribe(const String & topic_filter) override; + virtual SubscriptionId subscribe(const String & topic_filter, MessageCallback callback); + + SubscriptionId subscribe(const String & topic_filter, std::function callback, + size_t max_size = PICOMQTT_MAX_MESSAGE_SIZE); + + SubscriptionId subscribe(const String & topic_filter, std::function callback, + size_t max_size = PICOMQTT_MAX_MESSAGE_SIZE); + SubscriptionId subscribe(const String & topic_filter, std::function callback, + size_t max_size = PICOMQTT_MAX_MESSAGE_SIZE); + SubscriptionId subscribe(const String & topic_filter, std::function callback, + size_t max_size = PICOMQTT_MAX_MESSAGE_SIZE); + + virtual void unsubscribe(const String & topic_filter) override; + + virtual void on_extra_message(const char * topic, IncomingPacket & packet) {} + virtual void on_message_too_big(const char * topic, IncomingPacket & packet) {} + + protected: + void fire_message_callbacks(const char * topic, IncomingPacket & packet); + + std::map subscriptions; +}; + +} diff --git a/src/modules/exec/BrokerMQTT/modinfo.json b/src/modules/exec/BrokerMQTT/modinfo.json index 7cad1e16..3a5725f4 100644 --- a/src/modules/exec/BrokerMQTT/modinfo.json +++ b/src/modules/exec/BrokerMQTT/modinfo.json @@ -36,7 +36,6 @@ "defActive": false, "usedLibs": { "esp32_4mb3f": [], - "esp32*": [], - "esp82*": [] + "esp32*": [] } } \ No newline at end of file From b22860315859836680732dbe0c4126895ad40534 Mon Sep 17 00:00:00 2001 From: Mit4el Date: Tue, 12 Mar 2024 00:16:09 +0300 Subject: [PATCH 12/22] =?UTF-8?q?fix=20NTP=202902=20=D0=B8=20=D0=BC=D0=B5?= =?UTF-8?q?=D0=BB=D0=BA=D0=B8=D0=B5=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- myProfile.json | 6 +++++- src/NTP.cpp | 9 +++++++++ src/modules/exec/SmartBoiler/SmartBoiler.cpp | 5 +++-- src/modules/virtual/Benchmark/modinfo.json | 16 ++-------------- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/myProfile.json b/myProfile.json index 2b96cb10..e3875499 100644 --- a/myProfile.json +++ b/myProfile.json @@ -156,6 +156,10 @@ "path": "src/modules/virtual/owmWeather", "active": true }, + { + "path": "src/modules/virtual/Ping", + "active": true + }, { "path": "src/modules/virtual/Timer", "active": true @@ -294,7 +298,7 @@ "path": "src/modules/sensors/Mhz19", "active": false }, - { + { "path": "src/modules/sensors/MQgas", "active": true }, diff --git a/src/NTP.cpp b/src/NTP.cpp index 6d4a44e1..f43654ce 100644 --- a/src/NTP.cpp +++ b/src/NTP.cpp @@ -167,6 +167,15 @@ unsigned long strDateToUnix(String date) { int numberOfLeepYears = 12; int totalNormalYears = year - 1970 - numberOfLeepYears; unsigned int daysInMonth[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + if (year % 4 == 0) { + if (year % 100 != 0 || year % 400 == 0) { + daysInMonth[1] = 29; + } else { + daysInMonth[1] = 28; + } + } else { + daysInMonth[1] = 28; + } int numberOfDaysInPastMonths = 0; for (int i = 0; i <= 11; i++) { if (i <= month - 2) { diff --git a/src/modules/exec/SmartBoiler/SmartBoiler.cpp b/src/modules/exec/SmartBoiler/SmartBoiler.cpp index cd18db2d..e2821d5a 100644 --- a/src/modules/exec/SmartBoiler/SmartBoiler.cpp +++ b/src/modules/exec/SmartBoiler/SmartBoiler.cpp @@ -56,7 +56,8 @@ namespace _Boiler_v2 tmp = findIoTItem(widget); if (tmp) { - tmp->setValue(value, true); + if (value != tmp->value.valS) + tmp->setValue(value, true); } else { @@ -902,7 +903,7 @@ namespace _Boiler_v2 //} if (!state.stateDHW) // Если уже включено ГВС, то нечего смотреть на отопление { - if (set.cmd_chEnable && stepMap.size()>0) + if (set.cmd_chEnable && stepMap.size() > 0) { SerialPrint("I", "SmartBoiler", " stepMap.size = " + String(stepMap.size())); publishNew("status", "Штатный режим"); diff --git a/src/modules/virtual/Benchmark/modinfo.json b/src/modules/virtual/Benchmark/modinfo.json index d33a869c..538a62b6 100644 --- a/src/modules/virtual/Benchmark/modinfo.json +++ b/src/modules/virtual/Benchmark/modinfo.json @@ -51,19 +51,7 @@ "defActive": false, "usedLibs": { - "esp32_4mb": [], - "esp32_4mb3f": [], - "esp32s2_4mb": [], - "esp32_16mb": [], - "esp32s3_16mb": [], - "esp32c3m_4mb": [], - "esp8266_4mb": [], - "esp8266_16mb": [], - "esp8266_1mb": [], - "esp8266_1mb_ota": [], - "esp8285_1mb": [], - "esp8285_1mb_ota": [], - "esp8266_2mb": [], - "esp8266_2mb_ota": [] + "esp32*": [], + "esp82*": [] } } From cf099b0db60e5bde22e6cca65a17c4243b33b9e0 Mon Sep 17 00:00:00 2001 From: Mit4el Date: Sat, 16 Mar 2024 19:47:18 +0300 Subject: [PATCH 13/22] =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20=D0=BB?= =?UTF-8?q?=D0=B8=D1=88=D0=BD=D0=B5=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/ESPConfiguration.h | 1 - include/Global.h | 1 - src/Main.cpp | 1 + 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/include/ESPConfiguration.h b/include/ESPConfiguration.h index 7832008f..1e185219 100644 --- a/include/ESPConfiguration.h +++ b/include/ESPConfiguration.h @@ -7,4 +7,3 @@ extern std::list IoTItems; // вектор ссылок базово extern void configure(String path); void clearConfigure(); extern IoTItem* myIoTItem; -extern IoTBench* myIoTBernch; diff --git a/include/Global.h b/include/Global.h index ac94ead5..f748a4ba 100644 --- a/include/Global.h +++ b/include/Global.h @@ -50,7 +50,6 @@ #include "utils/StringUtils.h" #include "PeriodicTasks.h" #include "classes/IoTGpio.h" -#include "classes/IoTBench.h" /********************************************************************************************************************* *****************************************глобальные объекты классов*************************************************** diff --git a/src/Main.cpp b/src/Main.cpp index 6e076812..7f338d53 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -2,6 +2,7 @@ #include #include "classes/IoTDB.h" #include "utils/Statistic.h" +#include "classes/IoTBench.h" #include #if defined(esp32s2_4mb) || defined(esp32s3_16mb) #include From 403535b8d6b998064c2f65ec18974eb805026fb7 Mon Sep 17 00:00:00 2001 From: Mit4el Date: Sun, 17 Mar 2024 21:27:51 +0300 Subject: [PATCH 14/22] module SIM800 --- myProfile.json | 4 + src/modules/exec/SIM800/SIM800.cpp | 186 +++++++++++++++++++++++++++ src/modules/exec/SIM800/modinfo.json | 86 +++++++++++++ 3 files changed, 276 insertions(+) create mode 100644 src/modules/exec/SIM800/SIM800.cpp create mode 100644 src/modules/exec/SIM800/modinfo.json diff --git a/myProfile.json b/myProfile.json index e3875499..7e303157 100644 --- a/myProfile.json +++ b/myProfile.json @@ -432,6 +432,10 @@ "path": "src/modules/exec/SDcard", "active": false }, + { + "path": "src/modules/exec/SIM800", + "active": false + }, { "path": "src/modules/exec/SmartBoiler", "active": false diff --git a/src/modules/exec/SIM800/SIM800.cpp b/src/modules/exec/SIM800/SIM800.cpp new file mode 100644 index 00000000..9c868763 --- /dev/null +++ b/src/modules/exec/SIM800/SIM800.cpp @@ -0,0 +1,186 @@ + +#include "Global.h" +#include "classes/IoTUart.h" + +class Sim800 : public IoTUart +{ +private: + bool _debug; + String _number; + char _inc; + String _inStr = ""; // буфер приема строк + +public: + Sim800(String parameters) : IoTUart(parameters) + { + _number = jsonReadStr(parameters, "number"); + jsonRead(parameters, "debug", _debug); + } + + void _printUart(bool ln, String str) + { + if (!_myUART) + return; + if (!ln) + { + _myUART->print(str); + if (_debug) + SerialPrint("I", F("SIM800"), "<- print(" + str + ")"); + } + else + { + _myUART->println(str); + if (_debug) + SerialPrint("I", F("SIM800"), "<- println(" + str + ")"); + } + } + + void sendSms(String sms, String num) + { + _printUart(1, "AT+CMGF=1"); // переводим в текстовый режим + _printUart(1, "AT+CMGS=\"" + num + "\""); + _printUart(1, sms + "\r\n" + String((char)26)); + // _myUART->print((char)26); // код ctrl+c что является командой передачи сообщения + } + + void doByInterval() + { + if (_myUART) + { + // _myUART->println("AT"); // должен ответить OK + // _myUART->println("AT+CSQ"); // уровень сигнала в dB + // _myUART->println("AT+CCID"); // если есть сим карта, то вернет её номер + //_printUart(1, "AT+COPS?"); //+COPS: 0,0,"MTS-RUS" - оператор + _printUart(1, "AT+CPAS"); //+CPAS: 0 - готов к работе + //_printUart(1, "AT+CREG?"); // проверка регистрации в сети, второй паратетр: 1-регистрация в сети, 5-роуминг + // _myUART->println("ATI"); // имя модуля и его номер + } + } + + void uartHandle() + { + if (!_myUART) + return; + + if (_myUART->available()) + { + _inc = _myUART->read(); + + if (_inc == '\r') + { + _inStr += _inc; + if (_debug) + SerialPrint("I", F("SIM800"), "-> " + _inStr); + return; + } + + if (_inc == '\n') + { + // SerialPrint("I", F("SIM800"), "-> " + _inStr); + if (_inStr.indexOf("CPAS") != -1) + { + if (_inStr.indexOf("+CPAS: 0") != -1) + setValue("OK"); + else + setValue("NO"); + return; + } + } + else + _inStr += _inc; + } + } + + IoTValue execute(String command, std::vector ¶m) + { + if (!_myUART) + return {}; + if (command == "sms") + { + if (param.size() == 1) + { + sendSms(param[0].valS, _number); + } + else if (param.size() == 2) + { + sendSms(param[0].valS, param[1].valS); + } + } + // отправка кирилических символов на Nextion (русские буквы) + else if (command == "print") + { + if (param.size() == 2) + { + String strToUart = ""; + strToUart = param[0].valS; + + if (param[1].valD) + _printUart(0, "\"" + strToUart + "\""); + else + _printUart(0, strToUart); + } + if (param.size() == 3) + { + String strToUart = ""; + strToUart = param[0].valS; + + if (param[2].valD) + _printUart(0, strToUart + "\"" + param[1].valS + "\""); + else + _printUart(0, strToUart + param[1].valS); + } + } + else if (command == "println") + { + if (param.size() == 2) + { + String strToUart = ""; + strToUart = param[0].valS; + + if (param[1].valD) + _printUart(1, "\"" + strToUart + "\""); + else + _printUart(1, strToUart); + } + if (param.size() == 3) + { + String strToUart = ""; + strToUart = param[0].valS; + + if (param[2].valD) + _printUart(1, strToUart + "\"" + param[1].valS + "\""); + else + _printUart(1, strToUart + param[1].valS); + } + } + else if (command == "printHex") + { + unsigned char Hi, Lo; + uint8_t byteTx; + const char *strPtr = param[0].valS.c_str(); + while ((Hi = *strPtr++) && (Lo = *strPtr++)) + { + byteTx = (ChartoHex(Hi) << 4) | ChartoHex(Lo); + _myUART->write(byteTx); + } + if (_debug) + SerialPrint("I", F("SIM800"), "<- printHex(" + String(byteTx, HEX) + ")"); + } + + return {}; + } + + ~Sim800(){}; +}; + +void *getAPI_SIM800(String subtype, String param) +{ + if (subtype == F("sim800")) + { + return new Sim800(param); + } + else + { + return nullptr; + } +} diff --git a/src/modules/exec/SIM800/modinfo.json b/src/modules/exec/SIM800/modinfo.json new file mode 100644 index 00000000..959879ed --- /dev/null +++ b/src/modules/exec/SIM800/modinfo.json @@ -0,0 +1,86 @@ +{ + "menuSection": "executive_devices", + "configItem": [ + { + "global": 0, + "name": "SIM800", + "type": "Reading", + "subtype": "sim800", + "id": "sim", + "widget": "anydataDef", + "page": "Состояние", + "descr": "Sim в сети", + "int": 5, + "tx": 17, + "rx": 16, + "line": 2, + "speed": 115200, + "number": "+71234567890", + "debug": 0 + } + ], + "about": { + "authorName": "Bubnov Mikhail", + "authorContact": "https://t.me/Mit4bmw", + "authorGit": "https://github.com/Mit4el", + "specialThanks": "", + "moduleName": "SIM800", + "moduleVersion": "1.0", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "SIM800", + "moduleDesc": "Оправка AT-команд в модуль SIM800L. Отправка sms сообщений через модуль", + "propInfo": { + "int": "Период опроса состояния модуля SIM800", + "tx": "TX пин", + "rx": "RX пин", + "speed": "Скорость UART", + "line": "Актуально только для ESP32: номер линии hardUART. =2 rx=16 tx=17, для SoftwarwSerial в ESP32 line = -1", + "number": "Номер телефона для отправки sms сообщения" + }, + "funcInfo": [ + { + "name": "sms", + "descr": "Отправить sms сообщение. Например sim.sms(\"сообщение\") или sim.sms(\"сообщение\", \"+7999999\")", + "params": [ + "Строка текста", + "Номер телефона, если не указывать будет отправлено на номер number из конфигурации" + ] + }, + { + "name": "print", + "descr": "Отправить в UART строку текста (AT-команду). Например sim.print(\"AT\",0)", + "params": [ + "Строка текста", + "ID Виджета или любое значение, не обязательно", + "1 - обернуть строку в кавычки, 0 - отправить без кавычек (При наличии второго параметра оборачивает только его)" + ] + }, + { + "name": "println", + "descr": "Отправить в UART строку текста (AT-команду) и признак завершения строки (перевод строки). Например sim.println(\"AT+CMGS=\", \"+799999\", 1);", + "params": [ + "Строка текста", + "ID Виджета или любое значение, не обязательно", + "1 - обернуть строку в кавычки, 0 - отправить без кавычек (Пр наличии второго параметра оборачивает только его)" + ] + }, + { + "name": "printHex", + "descr": "Отправить в UART HEX-строку.", + "params": [ + "HEX-строка." + ] + } + ] + }, + "defActive": false, + "usedLibs": { + "esp32_4mb": [], + "esp32_4mb3f": [], + "esp32*": [], + "esp82*": [] + } +} \ No newline at end of file From bc62abded084e381857ba4b77af68d3027622b5c Mon Sep 17 00:00:00 2001 From: Mit4el Date: Sun, 17 Mar 2024 21:28:09 +0300 Subject: [PATCH 15/22] fix IoTUart --- src/classes/IoTUart.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/classes/IoTUart.cpp b/src/classes/IoTUart.cpp index 2ee9d384..d6678634 100644 --- a/src/classes/IoTUart.cpp +++ b/src/classes/IoTUart.cpp @@ -26,6 +26,7 @@ IoTUart::IoTUart(const String& parameters) : IoTItem(parameters) { void IoTUart::loop() { uartHandle(); + IoTItem::loop(); } void IoTUart::uartHandle() {} @@ -93,7 +94,7 @@ IoTValue IoTUart::execute(String command, std::vector ¶m) { if (param.size() == 1) { //if (param[0].isDecimal) uartPrint((String)param[0].valD); //else uartPrint(param[0].valS); - uartPrintln(param[0].valS); + uartPrint(param[0].valS); } } else if (command == "printHex") { if (param.size() == 1) { From 8c98a387a2cf5908150b740f3142d2b9f5439fa0 Mon Sep 17 00:00:00 2001 From: Mit4el Date: Tue, 19 Mar 2024 21:29:18 +0300 Subject: [PATCH 16/22] sim800 update --- src/modules/exec/SIM800/SIM800.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/modules/exec/SIM800/SIM800.cpp b/src/modules/exec/SIM800/SIM800.cpp index 9c868763..5a4edfbe 100644 --- a/src/modules/exec/SIM800/SIM800.cpp +++ b/src/modules/exec/SIM800/SIM800.cpp @@ -39,7 +39,10 @@ public: { _printUart(1, "AT+CMGF=1"); // переводим в текстовый режим _printUart(1, "AT+CMGS=\"" + num + "\""); - _printUart(1, sms + "\r\n" + String((char)26)); + //_printUart(1, sms + "\r\n" + String((char)26)); + _myUART->println(sms + "\r\n" + String((char)26)); + if (_debug) + SerialPrint("I", F("SIM800"), "<- println(" + sms + ")"); // _myUART->print((char)26); // код ctrl+c что является командой передачи сообщения } @@ -69,14 +72,9 @@ public: if (_inc == '\r') { _inStr += _inc; - if (_debug) + if (_debug && _inStr != "\r") SerialPrint("I", F("SIM800"), "-> " + _inStr); - return; - } - if (_inc == '\n') - { - // SerialPrint("I", F("SIM800"), "-> " + _inStr); if (_inStr.indexOf("CPAS") != -1) { if (_inStr.indexOf("+CPAS: 0") != -1) @@ -85,6 +83,13 @@ public: setValue("NO"); return; } + _inStr = ""; + return; + } + + if (_inc == '\n') + { + // SerialPrint("I", F("SIM800"), "-> " + _inStr); } else _inStr += _inc; From 9776f821d38a8ed79d113d20cd3abe7bc17887ab Mon Sep 17 00:00:00 2001 From: Mit4el Date: Thu, 21 Mar 2024 22:22:54 +0300 Subject: [PATCH 17/22] BrokerMqtt v2, SIM800 upd --- src/modules/exec/BrokerMQTT/BrokerMQTT.cpp | 97 ++++++++++++++++++---- src/modules/exec/BrokerMQTT/modinfo.json | 15 +++- src/modules/exec/SIM800/SIM800.cpp | 16 ++-- 3 files changed, 103 insertions(+), 25 deletions(-) diff --git a/src/modules/exec/BrokerMQTT/BrokerMQTT.cpp b/src/modules/exec/BrokerMQTT/BrokerMQTT.cpp index f3595345..9d62d553 100644 --- a/src/modules/exec/BrokerMQTT/BrokerMQTT.cpp +++ b/src/modules/exec/BrokerMQTT/BrokerMQTT.cpp @@ -7,16 +7,26 @@ namespace _Broker { #define DEF_PORT 1883 + // MqttBroker broker(1883); + class myPicoMQTT : public PicoMQTT::Server { private: bool _debug; + String _user; + String _pass; public: myPicoMQTT(int port) : PicoMQTT::Server(port) { } + void setAuth(String user, String pass) + { + _user = user; + _pass = pass; + } + void setDebug(bool debug) { _debug = debug; @@ -65,19 +75,36 @@ namespace _Broker Serial.println(topic); } } - }; - // MqttBroker broker(1883); - myPicoMQTT *picoMqtt = nullptr; - - myPicoMQTT *instanceBroker(int port) - { - if (!picoMqtt) + PicoMQTT::ConnectReturnCode auth(const char *client_id, const char *username, const char *password) { - picoMqtt = new myPicoMQTT(port); - // ot->begin(); + if (String(client_id).length() < 3) + { + return PicoMQTT::CRC_IDENTIFIER_REJECTED; + } + if (!username && !password) + { + return PicoMQTT::CRC_NOT_AUTHORIZED; + } + if (String(username) == _user && String(password) == _pass) + { + return PicoMQTT::CRC_ACCEPTED; + } + Serial.print("[BrokerMQTT], Client: "); + Serial.print(client_id); + Serial.print(", NOT Authorized: "); + Serial.print(username); + Serial.print(" != "); + Serial.print(_user); + Serial.print(" ,pass: "); + Serial.print(password); + Serial.print(" != "); + Serial.println(_pass); + return PicoMQTT::CRC_BAD_USERNAME_OR_PASSWORD; } - return picoMqtt; - } + }; + + myPicoMQTT *picoMqtt = nullptr; + PicoMQTT::Client *clientMqtt = nullptr; TaskHandle_t brokerTask; // void Task2code( void * pvParameters ); @@ -89,7 +116,10 @@ namespace _Broker Serial.println(xPortGetCoreID()); for (;;) { - instanceBroker(DEF_PORT)->loop(); + if (clientMqtt) + clientMqtt->loop(); + if (picoMqtt) + picoMqtt->loop(); // picoMqtt.loop(); // vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(5)); } @@ -100,14 +130,41 @@ namespace _Broker private: unsigned long ts = 0; int _port = 0; + String _user; + String _pass; bool _debug; + bool _brige; + String _server; + String _srvUser; + String _srvPass; + int _srvPort; public: BrokerMQTT(String parameters) : IoTItem(parameters) { SerialPrint("i", F("BrokerMQTT"), " START... "); jsonRead(parameters, "port", _port); + jsonRead(parameters, "user", _user); + jsonRead(parameters, "pass", _pass); jsonRead(parameters, "debug", _debug); + jsonRead(parameters, "brige", _brige); + jsonRead(parameters, "server", _server); + jsonRead(parameters, "srvUser", _srvUser); + jsonRead(parameters, "srvPass", _srvPass); + jsonRead(parameters, "srvPort", _srvPort); + + if (_brige) + { + clientMqtt = new PicoMQTT::Client(_server.c_str(), _srvPort, nullptr, _srvUser.c_str(), _srvPass.c_str()); + if (_debug) + { + SerialPrint("i", F("BrigeMQTT"), "Bridge mode : ON"); + SerialPrint("i", F("BrigeMQTT"), "Bridge server: " + _server); + SerialPrint("i", F("BrigeMQTT"), "Bridge port: " + String(_srvPort)); + SerialPrint("i", F("BrigeMQTT"), "Bridge user: " + _srvUser); + SerialPrint("i", F("BrigeMQTT"), "Bridge pass: " + _srvPass); + } + } } void doByInterval() @@ -117,8 +174,16 @@ namespace _Broker { if (!_port) _port = DEF_PORT; - instanceBroker(_port)->begin(); - instanceBroker(_port)->setDebug(_debug); + picoMqtt = new myPicoMQTT(_port); + picoMqtt->begin(); + picoMqtt->setDebug(_debug); + picoMqtt->setAuth(_user, _pass); + if (_brige && picoMqtt && clientMqtt) + { + picoMqtt->subscribe("#", [](const char *topic, const char *message) + { clientMqtt->publish(topic, message); + SerialPrint("i", F("BrigeMQTT"), "client publish, topic: " + String(topic) + " msg: " + String(message) ); }); + } // picoMqtt.begin(); xTaskCreatePinnedToCore( tBrokerMQTT, // Функция задачи. @@ -140,7 +205,9 @@ namespace _Broker ~BrokerMQTT() { - // delete picoMqtt; + vTaskDelete(brokerTask); + delete picoMqtt; + delete clientMqtt; } }; } diff --git a/src/modules/exec/BrokerMQTT/modinfo.json b/src/modules/exec/BrokerMQTT/modinfo.json index 3a5725f4..fac52f02 100644 --- a/src/modules/exec/BrokerMQTT/modinfo.json +++ b/src/modules/exec/BrokerMQTT/modinfo.json @@ -13,6 +13,13 @@ "int": 10, "value": "", "port": 1883, + "user": "root", + "pass": "4321", + "brige":1, + "server":"http://iotmanager.org", + "srvUser": "rise", + "srvPass": "3hostel3", + "srvPort": 1883, "debug": 1 } ], @@ -22,7 +29,7 @@ "authorGit": "https://github.com/Mit4el", "specialThanks": "Андрей Душин", "moduleName": "BrokerMQTT", - "moduleVersion": "0.1", + "moduleVersion": "2.0", "usedRam": { "esp32_4mb": 15, "esp8266_4mb": 15 @@ -30,7 +37,11 @@ "title": "BrokerMQTT", "moduleDesc": "MQTT Брокер на основе Pico Mqtt", "propInfo": { - "port":"Порт, по умолчанию 1883" + "port":"Порт, по умолчанию 1883", + "brige":"1 - Использовать режим моста, Брокер будет дублировать все топики в указанные сервер", + "server":"Адрес внешнего MQTT брокера/сервера для режима моста", + "srvUser": "Пользователь внешнего MQTT брокера", + "srvPass": "Пароль внешнего MQTT брокера" } }, "defActive": false, diff --git a/src/modules/exec/SIM800/SIM800.cpp b/src/modules/exec/SIM800/SIM800.cpp index 5a4edfbe..d5334c3f 100644 --- a/src/modules/exec/SIM800/SIM800.cpp +++ b/src/modules/exec/SIM800/SIM800.cpp @@ -38,7 +38,9 @@ public: void sendSms(String sms, String num) { _printUart(1, "AT+CMGF=1"); // переводим в текстовый режим + delay(2); _printUart(1, "AT+CMGS=\"" + num + "\""); + delay(2); //_printUart(1, sms + "\r\n" + String((char)26)); _myUART->println(sms + "\r\n" + String((char)26)); if (_debug) @@ -71,8 +73,12 @@ public: if (_inc == '\r') { - _inStr += _inc; - if (_debug && _inStr != "\r") + return; + } + if (_inc == '\n') + { + _inStr += "";//_inc; + if (_debug && _inStr != "") SerialPrint("I", F("SIM800"), "-> " + _inStr); if (_inStr.indexOf("CPAS") != -1) @@ -81,16 +87,10 @@ public: setValue("OK"); else setValue("NO"); - return; } _inStr = ""; return; } - - if (_inc == '\n') - { - // SerialPrint("I", F("SIM800"), "-> " + _inStr); - } else _inStr += _inc; } From b25398bff44e14d63e14fdff75dbd85657c089bf Mon Sep 17 00:00:00 2001 From: Mit4el Date: Fri, 29 Mar 2024 10:02:04 +0300 Subject: [PATCH 18/22] =?UTF-8?q?=D0=B4=D1=80=D0=B0=D0=B9=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=20Telegramm=20-=20tlgrmItem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/exec/Telegram/Telegram.cpp | 7 ++++++- src/modules/exec/TelegramLT/TelegramLT.cpp | 9 ++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/modules/exec/Telegram/Telegram.cpp b/src/modules/exec/Telegram/Telegram.cpp index 3ad5144e..da7186af 100644 --- a/src/modules/exec/Telegram/Telegram.cpp +++ b/src/modules/exec/Telegram/Telegram.cpp @@ -110,8 +110,13 @@ class Telegram : public IoTItem { } } - ~Telegram() { + IoTItem *getTlgrmDriver() + { + return this; + } + ~Telegram() { + tlgrmItem = nullptr; }; }; diff --git a/src/modules/exec/TelegramLT/TelegramLT.cpp b/src/modules/exec/TelegramLT/TelegramLT.cpp index eee738e0..44a7828d 100644 --- a/src/modules/exec/TelegramLT/TelegramLT.cpp +++ b/src/modules/exec/TelegramLT/TelegramLT.cpp @@ -58,7 +58,14 @@ class TelegramLT : public IoTItem { return {}; } - ~TelegramLT(){}; + IoTItem *getTlgrmDriver() + { + return this; + } + + ~TelegramLT() { + tlgrmItem = nullptr; + }; }; void *getAPI_TelegramLT(String subtype, String param) { From 604cb9677601ae436802f61219588dfca2b5a6bb Mon Sep 17 00:00:00 2001 From: Mit4el Date: Fri, 29 Mar 2024 10:05:06 +0300 Subject: [PATCH 19/22] patch time-out WebSocket esp32 --- platformio.ini | 7 +++++++ tools/patch32_ws.py | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 tools/patch32_ws.py diff --git a/platformio.ini b/platformio.ini index eaeb3c81..c28f99fe 100644 --- a/platformio.ini +++ b/platformio.ini @@ -180,6 +180,7 @@ build_src_filter = ${env:esp8266_16mb_fromitems.build_src_filter} [env:esp32_4mb] +extra_scripts = pre:tools/patch32_ws.py lib_deps = ${common_env_data.lib_deps_external} ${env:esp32_4mb_fromitems.lib_deps} @@ -200,6 +201,7 @@ build_src_filter = ${env:esp32_4mb_fromitems.build_src_filter} [env:esp32_4mb3f] +extra_scripts = pre:tools/patch32_ws.py lib_deps = ${common_env_data.lib_deps_external} ${env:esp32_4mb3f_fromitems.lib_deps} @@ -221,6 +223,7 @@ build_src_filter = ${env:esp32_4mb3f_fromitems.build_src_filter} [env:esp32cam_4mb] +extra_scripts = pre:tools/patch32_ws.py lib_deps = ${common_env_data.lib_deps_external} ${env:esp32cam_4mb_fromitems.lib_deps} @@ -244,6 +247,7 @@ build_src_filter = ${env:esp32cam_4mb_fromitems.build_src_filter} [env:esp32s2_4mb] +extra_scripts = pre:tools/patch32_ws.py lib_deps = ${common_env_data.lib_deps_external} ${env:esp32s2_4mb_fromitems.lib_deps} @@ -267,6 +271,7 @@ build_src_filter = ${env:esp32s2_4mb_fromitems.build_src_filter} [env:esp32c3m_4mb] +extra_scripts = pre:tools/patch32_ws.py lib_deps = ${common_env_data.lib_deps_external} ${env:esp32c3m_4mb_fromitems.lib_deps} @@ -289,6 +294,7 @@ build_src_filter = ${env:esp32c3m_4mb_fromitems.build_src_filter} [env:esp32s3_16mb] +extra_scripts = pre:tools/patch32_ws.py lib_deps = ${common_env_data.lib_deps_external} ${env:esp32s3_16mb_fromitems.lib_deps} @@ -313,6 +319,7 @@ build_src_filter = ${env:esp32s3_16mb_fromitems.build_src_filter} [env:esp32_16mb] +extra_scripts = pre:tools/patch32_ws.py lib_deps = ${common_env_data.lib_deps_external} ${env:esp32_16mb_fromitems.lib_deps} diff --git a/tools/patch32_ws.py b/tools/patch32_ws.py new file mode 100644 index 00000000..c30cfa43 --- /dev/null +++ b/tools/patch32_ws.py @@ -0,0 +1,27 @@ +# правим %USERPROFILE%\.platformio\packages\framework-arduinoespressif32\libraries\WiFi\src\WiFiClient.cpp 27-28 +# для уменьшения тайм-аута ВебСокетов +# #define WIFI_CLIENT_MAX_WRITE_RETRY (10) +# #define WIFI_CLIENT_SELECT_TIMEOUT_US (1000000) +# Прописать скрипт в platformio.ini внутри [env:esp32_4mb3f] написать extra_scripts = pre:tools/patch32_ws.py + +import os +import shutil +from sys import platform + +if platform == "linux" or platform == "linux2": + # linux + mainPyPath = '~/.platformio/packages/framework-arduinoespressif32/libraries/WiFi/src/WiFiClient.cpp' +else: + # windows + mainPyPath = os.environ['USERPROFILE'] + '\\.platformio\\packages\\framework-arduinoespressif32\\libraries\\WiFi\\src\\WiFiClient.cpp' + +# print(mainPyPath) + +with open(mainPyPath) as fr: + oldData = fr.read() + if not 'if WIFI_CLIENT_MAX_WRITE_RETRY (10)' in oldData: + shutil.copyfile(mainPyPath, mainPyPath+'.bak') + newData = oldData.replace('#define WIFI_CLIENT_MAX_WRITE_RETRY (10)', '#define WIFI_CLIENT_MAX_WRITE_RETRY (2)') + newData = newData.replace('#define WIFI_CLIENT_SELECT_TIMEOUT_US (1000000)', '#define WIFI_CLIENT_SELECT_TIMEOUT_US (500000)') + with open(mainPyPath, 'w') as fw: + fw.write(newData) \ No newline at end of file From 7a1fad9855c39fab14d5e84171d7fe4425bd43b1 Mon Sep 17 00:00:00 2001 From: Mit4el Date: Wed, 3 Apr 2024 09:59:48 +0300 Subject: [PATCH 20/22] =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20En?= =?UTF-8?q?ergyMonitor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- myProfile.json | 4 + .../sensors/EnergyMon485/Energy485Header.h | 48 +++ .../sensors/EnergyMon485/EnergyMon485.cpp | 340 ++++++++++++++++++ src/modules/sensors/EnergyMon485/modinfo.json | 83 +++++ 4 files changed, 475 insertions(+) create mode 100644 src/modules/sensors/EnergyMon485/Energy485Header.h create mode 100644 src/modules/sensors/EnergyMon485/EnergyMon485.cpp create mode 100644 src/modules/sensors/EnergyMon485/modinfo.json diff --git a/myProfile.json b/myProfile.json index 7e303157..9044ff30 100644 --- a/myProfile.json +++ b/myProfile.json @@ -242,6 +242,10 @@ "path": "src/modules/sensors/Emon", "active": false }, + { + "path": "src/modules/sensors/EnergyMon485", + "active": true + }, { "path": "src/modules/sensors/ExampleModule", "active": false diff --git a/src/modules/sensors/EnergyMon485/Energy485Header.h b/src/modules/sensors/EnergyMon485/Energy485Header.h new file mode 100644 index 00000000..c2349103 --- /dev/null +++ b/src/modules/sensors/EnergyMon485/Energy485Header.h @@ -0,0 +1,48 @@ +#pragma once + +namespace _Electro485 +{ + + unsigned char mass_to_send[16]; + unsigned char mass_read[16]; + unsigned short int checkSum; + + unsigned char auchCRCLo[] = { + 0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4, + 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, + 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, + 0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, + 0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7, + 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, + 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, + 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, + 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2, + 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, + 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB, + 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5, + 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 0x50, 0x90, 0x91, + 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C, + 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88, + 0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C, + 0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80, + 0x40}; + unsigned char auchCRCHi[] = { + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, + 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, + 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, + 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, + 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, + 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, + 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, + 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, + 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, + 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, + 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, + 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, + 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, + 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, + 0x40}; +} \ No newline at end of file diff --git a/src/modules/sensors/EnergyMon485/EnergyMon485.cpp b/src/modules/sensors/EnergyMon485/EnergyMon485.cpp new file mode 100644 index 00000000..c1fbe9b9 --- /dev/null +++ b/src/modules/sensors/EnergyMon485/EnergyMon485.cpp @@ -0,0 +1,340 @@ + +#include "Global.h" +#include "classes/IoTItem.h" + +#include "classes/IoTUart.h" +#include "Energy485Header.h" + +// PZEMContainer _pzemCntr; +Stream *_myUART_Gran = nullptr; + +namespace _Electro485 +{ + + unsigned short int sdm120_mbCRC16(unsigned char *puchMsg, unsigned char usDataLen) + { + unsigned char uchCRCHi = 0xFF; /* инициализация старшего байта контрольной суммы */ + unsigned char uchCRCLo = 0xFF; /* инициализация младшего байта контрольной суммы */ + unsigned char uIndex; + + uchCRCHi = 0xFF; + uchCRCLo = 0xFF; + + while (usDataLen--) + { + uIndex = uchCRCHi ^ *puchMsg++; + uchCRCHi = uchCRCLo ^ auchCRCHi[uIndex]; + uchCRCLo = auchCRCLo[uIndex]; + } + + return (uchCRCLo << 8 | uchCRCHi); + } + + unsigned short int gran485_mbCRC16(unsigned char *puchMsg, unsigned char usDataLen) + { + unsigned char uchCRCHi = 0xFF; /* инициализация старшего байта контрольной суммы */ + unsigned char uchCRCLo = 0xFF; /* инициализация младшего байта контрольной суммы */ + unsigned char uIndex; + + uchCRCHi = 0xFF; + uchCRCLo = 0xFF; + + while (usDataLen--) + { + uIndex = (uchCRCHi ^ *puchMsg++) & 0xFF; + uchCRCHi = uchCRCLo ^ auchCRCHi[uIndex]; + uchCRCLo = auchCRCLo[uIndex]; + } + + return (uchCRCHi << 8 | uchCRCLo); + } + + void uart485_send(unsigned char *str, unsigned char bytes_to_send) + { + // delay(20); + if (_myUART_Gran) + { + for (int i = 0; i < bytes_to_send; i++) + { + _myUART_Gran->write(str[i]); + } + } + // delay(100); + } + + int8_t gran485_read(byte addr, String ¶m, float &res) + { + int8_t ret; + if (!_myUART_Gran) + return 3; + unsigned short int param_hex; + mass_to_send[4] = 0x00; + if (param == "v") + param_hex = 0x0A00; + else if (param == "a") + param_hex = 0x0B00; + else if (param == "w") + param_hex = 0x0800; + else if (param == "r") + param_hex = 0x0900; + else if (param == "f") + param_hex = 0x0D00; + else if (param == "k") + { + param_hex = 0x0100; + mass_to_send[4] = 0x01; + } + else if (param == "p") + { + param_hex = 0x0C00; + } + SerialPrint("i", "Gran", "param: " + param + ", param_hex: " + String(param_hex, HEX)); + mass_to_send[0] = addr; + mass_to_send[1] = 0x03; + mass_to_send[2] = param_hex >> 8; + mass_to_send[3] = param_hex; + + mass_to_send[5] = 0x01; + checkSum = gran485_mbCRC16(mass_to_send, 6); + mass_to_send[6] = checkSum; + mass_to_send[7] = checkSum >> 8; + uart485_send(mass_to_send, 8); + + // Считываем первые 3 байта из ответа + int s = _myUART_Gran->readBytes(mass_read, 10); + // uart_send(mass_read, 3); + // Serial.println("Count read byte: " + String(s)); + + // Если вернулся правильный адрес и команда + if (mass_read[0] == addr && mass_read[1] == 0x03) + { + // Проверяем контрольную сумму + checkSum = 0; + checkSum = gran485_mbCRC16(mass_read, 8); + /* + Serial.print("HEX: "); + Serial.print(String(mass_read[4], HEX)); + Serial.print(String(mass_read[5], HEX)); + Serial.print(String(mass_read[6], HEX)); + Serial.println(String(mass_read[7], HEX)); + */ + uint32_t x = mass_read[7] << 24 | mass_read[6] << 16 | mass_read[5] << 8 | mass_read[4]; + float y = *(float *)&x; + Serial.println(String(y)); + if ((byte)checkSum == mass_read[8] && (checkSum >> 8) == mass_read[9]) + { + res = y; + ret = 0; + } + else + { + // Serial.println("ERROR_CRC"); + ret = 1; + } + } + else + { + // Serial.println("ERROR_NO_RESPONSE"); + ret = 2; + } + + // очистка массива + mass_read[0] = 0; + mass_read[1] = 0; + mass_read[2] = 0; + mass_read[3] = 0; + mass_read[4] = 0; + mass_read[5] = 0; + mass_read[6] = 0; + // очистка буфера serial + while (_myUART_Gran->available()) + _myUART_Gran->read(); + return ret; + } + + int8_t sdm120_read(byte addr, String ¶m, float &res) + { + int8_t ret; + if (!_myUART_Gran) + return 3; + unsigned short int param_hex; + if (param == "v") + param_hex = 0x0000; + else if (param == "a") + param_hex = 0x0006; + else if (param == "w") + param_hex = 0x000C; + else if (param == "r") + param_hex = 0x0018; + else if (param == "f") + param_hex = 0x0046; + else if (param == "k") + param_hex = 0x0156; + else if (param == "p") + param_hex = 0x001E; + SerialPrint("i", "SMD120", "param: " + param + ", param_hex: " + String(param_hex, HEX)); + + mass_to_send[0] = addr; + mass_to_send[1] = 0x04; + mass_to_send[2] = param_hex >> 8; + mass_to_send[3] = param_hex; + mass_to_send[4] = 0x00; + mass_to_send[5] = 0x02; + checkSum = sdm120_mbCRC16(mass_to_send, 6); + mass_to_send[6] = checkSum; + mass_to_send[7] = checkSum >> 8; + uart485_send(mass_to_send, 8); + + // Считываем первые 3 байта из ответа + _myUART_Gran->readBytes(mass_read, 3); + // uart_send(mass_read, 3); + + // Если вернулся правильный адрес и команда + if (mass_read[0] == addr && mass_read[1] == 0x04) + { + // то считываем данные (их количество было в тетьем байте) +2 байта на контрольную сумму +3 байта чтобы не затереть начало массива + for (int i = 3; i < mass_read[2] + 5; i++) + { + mass_read[i] = _myUART_Gran->read(); + } + + // Проверяем контрольную сумму + checkSum = 0; + checkSum = sdm120_mbCRC16(mass_read, mass_read[2] + 3); + if ((byte)checkSum == mass_read[mass_read[2] + 3] && (checkSum >> 8) == mass_read[mass_read[2] + 4]) + { + // преобразуем результат во float + + uint32_t x = mass_read[3] << 24 | mass_read[4] << 16 | mass_read[5] << 8 | mass_read[6]; + float y = *(float *)&x; + res = y; + + ret = 0; + } + else + { + // Serial.println("ERROR_CRC"); + ret = 1; + } + } + else + { + // Serial.println("ERROR_NO_RESPONSE"); + ret = 2; + } + + // очистка массива + mass_read[0] = 0; + mass_read[1] = 0; + mass_read[2] = 0; + mass_read[3] = 0; + mass_read[4] = 0; + mass_read[5] = 0; + mass_read[6] = 0; + + // очистка буфера serial + while (_myUART_Gran->available()) + _myUART_Gran->read(); + return ret; + } + + class GranItem : public IoTItem + { + private: + String _sensor; + + public: + GranItem(String parameters) : IoTItem(parameters) + { + _sensor = jsonReadStr(parameters, "sensor"); + } + + void doByInterval() + { + + byte addr = 00; + float val = 0; + int8_t result = gran485_read(addr, _sensor, val); + + if (result == 0) // OK + regEvent(val, "Gran"); + else + { + regEvent(NAN, "Gran"); + if (result == 1) // ERROR_CRC + SerialPrint("E", "Gran", "ERROR_CRC", _id); + else if (result == 2) // ERROR_NO_RESPONSE + SerialPrint("E", "Gran", "ERROR_NO_RESPONSE", _id); + else if (result == 3) // (!_myUART_Gran) + SerialPrint("E", "Gran", "gran_uart not found", _id); + } + } + + ~GranItem(){}; + }; + + class SDM120Item : public IoTItem + { + private: + String _sensor; + + public: + SDM120Item(String parameters) : IoTItem(parameters) + { + _sensor = jsonReadStr(parameters, "sensor"); + } + + void doByInterval() + { + + byte addr = 00; + float val = 0; + int8_t result = sdm120_read(addr, _sensor, val); + + if (result == 0) // OK + regEvent(val, "SDM120"); + else + { + regEvent(NAN, "SDM120"); + if (result == 1) // ERROR_CRC + SerialPrint("E", "SDM120", "ERROR_CRC", _id); + else if (result == 2) // ERROR_NO_RESPONSE + SerialPrint("E", "SDM120", "ERROR_NO_RESPONSE", _id); + else if (result == 3) // (!_myUART_Gran) + SerialPrint("E", "SDM120", "gran_uart not found", _id); + } + } + + ~SDM120Item(){}; + }; + + class EnergyUART : public IoTUart + { + public: + EnergyUART(String parameters) : IoTUart(parameters) + { + _myUART_Gran = _myUART; + } + + ~EnergyUART(){}; + }; +} +void *getAPI_EnergyMon485(String subtype, String param) +{ + if (subtype == F("gran485")) + { + return new _Electro485::GranItem(param); + } + else if (subtype == F("sdm120")) + { + return new _Electro485::SDM120Item(param); + } + else if (subtype == F("energy_uart")) + { + return new _Electro485::EnergyUART(param); + } + else + { + return nullptr; + } +} diff --git a/src/modules/sensors/EnergyMon485/modinfo.json b/src/modules/sensors/EnergyMon485/modinfo.json new file mode 100644 index 00000000..233bfabc --- /dev/null +++ b/src/modules/sensors/EnergyMon485/modinfo.json @@ -0,0 +1,83 @@ +{ + "menuSection": "sensors", + "configItem": [ + { + "global": 0, + "name": "Gran данные", + "type": "Reading", + "subtype": "gran485", + "id": "gran", + "widget": "anydataVlt", + "page": "Гран-Электро", + "descr": "Потребление", + "sensor": "k", + "int": 15, + "round": 1 + }, + { + "global": 0, + "name": "SDM120 данные", + "type": "Reading", + "subtype": "sdm120", + "id": "sdm120", + "widget": "anydataVlt", + "page": "Счётчик SDM120", + "descr": "Потребление", + "sensor": "k", + "int": 15, + "round": 1 + }, + { + "global": 0, + "name": "Energy UART", + "type": "Reading", + "subtype": "energy_uart", + "id": "enrg_uart", + "widget": "nil", + "page": "", + "descr": "", + "tx": 17, + "rx": 16, + "line": 2, + "speed": 9600 + } + ], + "about": { + "authorName": "Bubnov Mikhail", + "authorContact": "https://t.me/Mit4bmw", + "authorGit": "https://github.com/Mit4el", + "specialThanks": "", + "moduleName": "EnergyMon485", + "moduleVersion": "1.0", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "Счетчик электроэнергии Гран-Электро или SDM120", + "moduleDesc": "Счетчик электроэнергии Гран-Электро (Беларусь) или SDM120 (Китай) с интерфейсом rs-485. Energy UART - обязателен, настройки UART интерфейса модуля", + "propInfo": { + "tx": "TX пин", + "rx": "RX пин", + "speed": "Скорость UART", + "line": "Актуально только для ESP32: номер линии hardUART. =2 rx=16 tx=17, для SoftwarwSerial в ESP32 line = -1", + "sensor": "Тип данных: v - напряжение, a - ток, w - активная мощность, r - реактивная мощность, f - частота, k - общее потребление, p - косинус фи", + "int": "Количество секунд между опросами датчика. Желателно устанавливать одинаковые интервалы для параметров (для одного адреса Pzem) что опрос происходил один раз, остальные из 1000мс буфера." + } + }, + "defActive": false, + "usedLibs": { + "esp32*": [], + "esp82*": [], + "esp32_4mb": [], + "esp32_4mb3f": [], + "esp32cam_4mb": [], + "esp32c3m_4mb": [], + "esp8266_4mb": [], + "esp8266_1mb": [], + "esp8266_1mb_ota": [], + "esp8285_1mb": [], + "esp8285_1mb_ota": [], + "esp8266_2mb": [], + "esp8266_2mb_ota": [] + } +} \ No newline at end of file From d95907a839d198e4789d5fbeb7aa912aef18fb1f Mon Sep 17 00:00:00 2001 From: Mit4el Date: Tue, 16 Apr 2024 23:31:23 +0300 Subject: [PATCH 21/22] new BL0937 --- src/modules/sensors/BL0937/BL0937.cpp | 269 +++++++++++++++++++++++ src/modules/sensors/BL0937/BL0937lib.cpp | 238 ++++++++++++++++++++ src/modules/sensors/BL0937/BL0937lib.h | 146 ++++++++++++ src/modules/sensors/BL0937/modinfo.json | 129 +++++++++++ 4 files changed, 782 insertions(+) create mode 100644 src/modules/sensors/BL0937/BL0937.cpp create mode 100644 src/modules/sensors/BL0937/BL0937lib.cpp create mode 100644 src/modules/sensors/BL0937/BL0937lib.h create mode 100644 src/modules/sensors/BL0937/modinfo.json diff --git a/src/modules/sensors/BL0937/BL0937.cpp b/src/modules/sensors/BL0937/BL0937.cpp new file mode 100644 index 00000000..2e207813 --- /dev/null +++ b/src/modules/sensors/BL0937/BL0937.cpp @@ -0,0 +1,269 @@ + +#include "Global.h" +#include "classes/IoTItem.h" + +#include "BL0937lib.h" +// #include "classes/IoTUart.h" +// #include +BL0937 *bl0937 = nullptr; + +class BL0937v : public IoTItem +{ +private: +public: + BL0937v(String parameters) : IoTItem(parameters) + { + } + + void doByInterval() + { + if (bl0937) + regEvent(bl0937->getVoltage(), "BL0937 V"); + else + { + regEvent(NAN, "BL0937v"); + SerialPrint("E", "BL0937cmd", "initialization error", _id); + } + } + + ~BL0937v(){}; +}; + +class BL0937a : public IoTItem +{ +private: +public: + BL0937a(String parameters) : IoTItem(parameters) + { + } + + void doByInterval() + { + if (bl0937) + regEvent(bl0937->getCurrent(), "BL0937 A"); + else + { + regEvent(NAN, "BL0937a"); + SerialPrint("E", "BL0937cmd", "initialization error", _id); + } + } + + ~BL0937a(){}; +}; + +class BL0937w : public IoTItem +{ +private: +public: + BL0937w(String parameters) : IoTItem(parameters) + { + } + + void doByInterval() + { + if (bl0937) + regEvent(bl0937->getApparentPower(), "BL0937 W"); + else + { + regEvent(NAN, "BL0937w"); + SerialPrint("E", "BL0937cmd", "initialization error", _id); + } + } + + ~BL0937w(){}; +}; + +class BL0937reactw : public IoTItem +{ +private: +public: + BL0937reactw(String parameters) : IoTItem(parameters) + { + } + + void doByInterval() + { + if (bl0937) + regEvent(bl0937->getReactivePower(), "BL0937 reactW"); + else + { + regEvent(NAN, "BL0937reactw"); + SerialPrint("E", "BL0937cmd", "initialization error", _id); + } + } + + ~BL0937reactw(){}; +}; + +class BL0937actw : public IoTItem +{ +private: +public: + BL0937actw(String parameters) : IoTItem(parameters) + { + } + + void doByInterval() + { + if (bl0937) + regEvent(bl0937->getActivePower(), "BL0937 actW"); + else + { + regEvent(NAN, "BL0937actw"); + SerialPrint("E", "BL0937cmd", "initialization error", _id); + } + } + + ~BL0937actw(){}; +}; + +class BL0937wh : public IoTItem +{ +private: +public: + BL0937wh(String parameters) : IoTItem(parameters) + { + } + + void doByInterval() + { + if (bl0937) + regEvent(bl0937->getEnergy() / 3600.0 / 1000.0, "BL0937 Wh"); + else + { + regEvent(NAN, "BL0937wh"); + SerialPrint("E", "BL0937cmd", "initialization error", _id); + } + } + + ~BL0937wh(){}; +}; + +void ICACHE_RAM_ATTR bl0937_cf1_interrupt() +{ + bl0937->cf1_interrupt(); +} +void ICACHE_RAM_ATTR bl0937_cf_interrupt() +{ + bl0937->cf_interrupt(); +} + +class BL0937cmd : public IoTItem +{ +private: + float CURRENT_RESISTOR = 0.001; // Нужна возможность задавать из веб, это по умолчанию + int VOLTAGE_RESISTOR_UPSTREAM = 1000000; // Нужна возможность задавать из веб, это по умолчанию + int VOLTAGE_RESISTOR_DOWNSTREAM = 1000; // Нужна возможность задавать из веб, это по умолчанию + int BL0937_CF_GPIO = 4; // 8266 12 //Нужна возможность задавать пин из веб, это по умолчанию + int BL0937_CF1_GPIO = 5; // 8266 13 //Нужна возможность задавать пин из веб, это по умолчанию + int BL0937_SEL_GPIO_INV = 12; // 8266 15 // inverted //Нужна возможность задавать пин из веб, это по умолчанию + float _expV = 0; + float _expA = 0; + float _expW = 0; + +public: + BL0937cmd(String parameters) : IoTItem(parameters) + { + jsonRead(parameters, "R_current", CURRENT_RESISTOR); + jsonRead(parameters, "R_upstream", VOLTAGE_RESISTOR_UPSTREAM); + jsonRead(parameters, "R_downstream", VOLTAGE_RESISTOR_DOWNSTREAM); + jsonRead(parameters, "CF_GPIO", BL0937_CF_GPIO); + jsonRead(parameters, "CF1_GPIO", BL0937_CF1_GPIO); + jsonRead(parameters, "SEL_GPIO", BL0937_SEL_GPIO_INV); + jsonRead(parameters, "expV", _expV); + jsonRead(parameters, "expA", _expA); + jsonRead(parameters, "expW", _expW); + bl0937 = new BL0937; + bl0937->begin(BL0937_CF_GPIO, BL0937_CF1_GPIO, BL0937_SEL_GPIO_INV, LOW, true); + bl0937->setResistors(CURRENT_RESISTOR, VOLTAGE_RESISTOR_UPSTREAM, VOLTAGE_RESISTOR_DOWNSTREAM); + attachInterrupt(BL0937_CF1_GPIO, bl0937_cf1_interrupt, FALLING); + attachInterrupt(BL0937_CF_GPIO, bl0937_cf_interrupt, FALLING); + if (_expV) + bl0937->expectedVoltage(_expV); // для калибровки вольтаж нужно вводить из веб интерфейса + if (_expV) + bl0937->expectedCurrent(_expA); // для калибровки можно так, а лучше ток вводить из веб интерфейса + if (_expV) + bl0937->expectedActivePower(_expW); // для калибровки потребляемую мощность нужно вводить из веб интерфейса + } + + void doByInterval() + { + } + + void onModuleOrder(String &key, String &value) + { + if (bl0937) + { + if (key == "reset") + { + bl0937->resetEnergy(); + SerialPrint("i", "BL0937", "reset energy done"); + } + } + } + /* + IoTValue execute(String command, std::vector ¶m) + { + if (!bl0937) + return {}; + if (command == "calibration") + { + if (param.size() == 3) + { + float v = param[0].valD; + float a = param[1].valD; + float p = param[2].valD; + bl0937->expectedVoltage(v); // для калибровки вольтаж нужно вводить из веб интерфейса + bl0937->expectedCurrent(a); // для калибровки можно так, а лучше ток вводить из веб интерфейса + bl0937->expectedActivePower(p); // для калибровки потребляемую мощность нужно вводить из веб интерфейса + return {}; + } + } + return {}; + } + */ + ~BL0937cmd() + { + if (bl0937) + { + delete bl0937; + bl0937 = nullptr; + } + }; +}; + +void *getAPI_BL0937(String subtype, String param) +{ + if (subtype == F("BL0937v")) + { + return new BL0937v(param); + } + else if (subtype == F("BL0937a")) + { + return new BL0937a(param); + } + else if (subtype == F("BL0937w")) + { + return new BL0937w(param); + } + else if (subtype == F("BL0937wh")) + { + return new BL0937wh(param); + } + else if (subtype == F("BL0937reactw")) + { + return new BL0937reactw(param); + } + else if (subtype == F("BL0937actw")) + { + return new BL0937actw(param); + } + else if (subtype == F("BL0937cmd")) + { + return new BL0937cmd(param); + } + else + { + return nullptr; + } +} diff --git a/src/modules/sensors/BL0937/BL0937lib.cpp b/src/modules/sensors/BL0937/BL0937lib.cpp new file mode 100644 index 00000000..d4b46e55 --- /dev/null +++ b/src/modules/sensors/BL0937/BL0937lib.cpp @@ -0,0 +1,238 @@ +/* + +BL0937 + +Copyright (C) 2016-2018 by Xose Pérez + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include +#include "BL0937lib.h" + +void BL0937::begin( + unsigned char cf_pin, + unsigned char cf1_pin, + unsigned char sel_pin, + unsigned char currentWhen, + bool use_interrupts, + unsigned long pulse_timeout + ) { + + _cf_pin = cf_pin; + _cf1_pin = cf1_pin; + _sel_pin = sel_pin; + _current_mode = currentWhen; + _use_interrupts = use_interrupts; + _pulse_timeout = pulse_timeout; + + pinMode(_cf_pin, INPUT_PULLUP); + pinMode(_cf1_pin, INPUT_PULLUP); + pinMode(_sel_pin, OUTPUT); + + _calculateDefaultMultipliers(); + + _mode = _current_mode; + digitalWrite(_sel_pin, _mode); + + +} + +void BL0937::setMode(bl0937_mode_t mode) { + _mode = (mode == MODE_CURRENT) ? _current_mode : 1 - _current_mode; + digitalWrite(_sel_pin, _mode); + if (_use_interrupts) { + _last_cf1_interrupt = _first_cf1_interrupt = micros(); + } +} + +bl0937_mode_t BL0937::getMode() { + return (_mode == _current_mode) ? MODE_CURRENT : MODE_VOLTAGE; +} + +bl0937_mode_t BL0937::toggleMode() { + bl0937_mode_t new_mode = getMode() == MODE_CURRENT ? MODE_VOLTAGE : MODE_CURRENT; + setMode(new_mode); + return new_mode; +} + +double BL0937::getCurrent() { + + // Power measurements are more sensitive to switch offs, + // so we first check if power is 0 to set _current to 0 too + if (_power == 0) { + _current_pulse_width = 0; + + } else if (_use_interrupts) { + _checkCF1Signal(); + + } else if (_mode == _current_mode) { + _current_pulse_width = pulseIn(_cf1_pin, HIGH, _pulse_timeout); + } + + _current = (_current_pulse_width > 0) ? _current_multiplier / _current_pulse_width : 0; + return _current; + +} + +unsigned int BL0937::getVoltage() { + if (_use_interrupts) { + _checkCF1Signal(); + } else if (_mode != _current_mode) { + _voltage_pulse_width = pulseIn(_cf1_pin, HIGH, _pulse_timeout); + } + _voltage = (_voltage_pulse_width > 0) ? _voltage_multiplier / _voltage_pulse_width : 0; + return _voltage; +} + +unsigned int BL0937::getActivePower() { + if (_use_interrupts) { + _checkCFSignal(); + } else { + _power_pulse_width = pulseIn(_cf_pin, HIGH, _pulse_timeout); + } + _power = (_power_pulse_width > 0) ? _power_multiplier / _power_pulse_width : 0; + return _power; +} + +unsigned int BL0937::getApparentPower() { + double current = getCurrent(); + unsigned int voltage = getVoltage(); + return voltage * current; +} + +unsigned int BL0937::getReactivePower() { + unsigned int active = getActivePower(); + unsigned int apparent = getApparentPower(); + if (apparent > active) { + return sqrt(apparent * apparent - active * active); + } else { + return 0; + } +} + +double BL0937::getPowerFactor() { + unsigned int active = getActivePower(); + unsigned int apparent = getApparentPower(); + if (active > apparent) return 1; + if (apparent == 0) return 0; + return (double) active / apparent; +} + +unsigned long BL0937::getEnergy() { + + // Counting pulses only works in IRQ mode + if (!_use_interrupts) return 0; + + /* + Pulse count is directly proportional to energy: + P = m*f (m=power multiplier, f = Frequency) + f = N/t (N=pulse count, t = time) + E = P*t = m*N (E=energy) + */ + return _pulse_count * _power_multiplier / 1000000.0; + +} + +void BL0937::resetEnergy() { + _pulse_count = 0; +} + +void BL0937::expectedCurrent(double value) { + if (_current == 0) getCurrent(); + if (_current > 0) _current_multiplier *= (value / _current); +} + +void BL0937::expectedVoltage(unsigned int value) { + if (_voltage == 0) getVoltage(); + if (_voltage > 0) _voltage_multiplier *= ((double) value / _voltage); +} + +void BL0937::expectedActivePower(unsigned int value) { + if (_power == 0) getActivePower(); + if (_power > 0) _power_multiplier *= ((double) value / _power); +} + +void BL0937::resetMultipliers() { + _calculateDefaultMultipliers(); +} + +void BL0937::setResistors(double current, double voltage_upstream, double voltage_downstream) { + if (voltage_downstream > 0) { + _current_resistor = current; + _voltage_resistor = (voltage_upstream + voltage_downstream) / voltage_downstream; + _calculateDefaultMultipliers(); + } +} + +void ICACHE_RAM_ATTR BL0937::cf_interrupt() { + unsigned long now = micros(); + _power_pulse_width = now - _last_cf_interrupt; + _last_cf_interrupt = now; + _pulse_count++; +} + +void ICACHE_RAM_ATTR BL0937::cf1_interrupt() { + + unsigned long now = micros(); + + if ((now - _first_cf1_interrupt) > _pulse_timeout) { + + unsigned long pulse_width; + + if (_last_cf1_interrupt == _first_cf1_interrupt) { + pulse_width = 0; + } else { + pulse_width = now - _last_cf1_interrupt; + } + + if (_mode == _current_mode) { + _current_pulse_width = pulse_width; + } else { + _voltage_pulse_width = pulse_width; + } + + _mode = 1 - _mode; + digitalWrite(_sel_pin, _mode); + _first_cf1_interrupt = now; + + } + + _last_cf1_interrupt = now; + +} + +void BL0937::_checkCFSignal() { + if ((micros() - _last_cf_interrupt) > _pulse_timeout) _power_pulse_width = 0; +} + +void BL0937::_checkCF1Signal() { + if ((micros() - _last_cf1_interrupt) > _pulse_timeout) { + if (_mode == _current_mode) { + _current_pulse_width = 0; + } else { + _voltage_pulse_width = 0; + } + toggleMode(); + } +} + +// These are the multipliers for current, voltage and power as per datasheet +// These values divided by output period (in useconds) give the actual value +void BL0937::_calculateDefaultMultipliers() { + _current_multiplier = ( 1000000.0 * 512 * V_REF / _current_resistor / 24.0 / F_OSC ) * 0.850112464f; + _voltage_multiplier = ( 1000000.0 * 512 * V_REF * _voltage_resistor / 2.0 / F_OSC) * 0.863158011f; + _power_multiplier = ( 1000000.0 * 128 * V_REF * V_REF * _voltage_resistor / _current_resistor / 48.0 / F_OSC) * 0.713465334f; +} diff --git a/src/modules/sensors/BL0937/BL0937lib.h b/src/modules/sensors/BL0937/BL0937lib.h new file mode 100644 index 00000000..f69295ae --- /dev/null +++ b/src/modules/sensors/BL0937/BL0937lib.h @@ -0,0 +1,146 @@ +/* + +BL0937 + +Copyright (C) 2016-2018 by Xose Pérez + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#ifndef BL0937_h +#define BL0937_h + +#include + +// Internal voltage reference value +#define V_REF 1.218 + +// The factor of a 1mOhm resistor +// as per recomended circuit in datasheet +// A 1mOhm resistor allows a ~30A max measurement +#define R_CURRENT 0.001 + +// This is the factor of a voltage divider of 1MOhm upstream and 1kOhm downstream +// as per recomended circuit in datasheet +#define R_VOLTAGE 1000 + +// Frequency of the BL0937 internal clock +#define F_OSC 2000000 + +// Minimum delay between selecting a mode and reading a sample +#define READING_INTERVAL 3000 + +// Maximum pulse with in microseconds +// If longer than this pulse width is reset to 0 +// This value is purely experimental. +// Higher values allow for a better precission but reduce sampling rate +// and response speed to change +// Lower values increase sampling rate but reduce precission +// Values below 0.5s are not recommended since current and voltage output +// will have no time to stabilise +#define PULSE_TIMEOUT 5000000 + +// Define ICACHE_RAM_ATTR for AVR platforms +#if defined(ARDUINO_ARCH_AVR) +#define ICACHE_RAM_ATTR +#endif + +// CF1 mode +typedef enum { + MODE_CURRENT, + MODE_VOLTAGE +} bl0937_mode_t; + +class BL0937 { + + public: + + void cf_interrupt(); + void cf1_interrupt(); + + void begin( + unsigned char cf_pin, + unsigned char cf1_pin, + unsigned char sel_pin, + unsigned char currentWhen = HIGH, + bool use_interrupts = true, + unsigned long pulse_timeout = PULSE_TIMEOUT); + + void setMode(bl0937_mode_t mode); + bl0937_mode_t getMode(); + bl0937_mode_t toggleMode(); + + double getCurrent(); + unsigned int getVoltage(); + unsigned int getActivePower(); + unsigned int getApparentPower(); + double getPowerFactor(); + unsigned int getReactivePower(); + unsigned long getEnergy(); //in Ws + void resetEnergy(); + + void setResistors(double current, double voltage_upstream, double voltage_downstream); + + void expectedCurrent(double current); + void expectedVoltage(unsigned int current); + void expectedActivePower(unsigned int power); + + double getCurrentMultiplier() { return _current_multiplier; }; + double getVoltageMultiplier() { return _voltage_multiplier; }; + double getPowerMultiplier() { return _power_multiplier; }; + + void setCurrentMultiplier(double current_multiplier) { _current_multiplier = current_multiplier; }; + void setVoltageMultiplier(double voltage_multiplier) { _voltage_multiplier = voltage_multiplier; }; + void setPowerMultiplier(double power_multiplier) { _power_multiplier = power_multiplier; }; + void resetMultipliers(); + + private: + + unsigned char _cf_pin; + unsigned char _cf1_pin; + unsigned char _sel_pin; + + double _current_resistor = R_CURRENT; + double _voltage_resistor = R_VOLTAGE; + + double _current_multiplier; // Unit: us/A + double _voltage_multiplier; // Unit: us/V + double _power_multiplier; // Unit: us/W + + unsigned long _pulse_timeout = PULSE_TIMEOUT; //Unit: us + volatile unsigned long _voltage_pulse_width = 0; //Unit: us + volatile unsigned long _current_pulse_width = 0; //Unit: us + volatile unsigned long _power_pulse_width = 0; //Unit: us + volatile unsigned long _pulse_count = 0; + + double _current = 0; + unsigned int _voltage = 0; + unsigned int _power = 0; + + unsigned char _current_mode = HIGH; + volatile unsigned char _mode; + + bool _use_interrupts = true; + volatile unsigned long _last_cf_interrupt = 0; + volatile unsigned long _last_cf1_interrupt = 0; + volatile unsigned long _first_cf1_interrupt = 0; + + void _checkCFSignal(); + void _checkCF1Signal(); + void _calculateDefaultMultipliers(); + +}; + +#endif diff --git a/src/modules/sensors/BL0937/modinfo.json b/src/modules/sensors/BL0937/modinfo.json new file mode 100644 index 00000000..d9be43f7 --- /dev/null +++ b/src/modules/sensors/BL0937/modinfo.json @@ -0,0 +1,129 @@ +{ + "menuSection": "sensors", + "configItem": [ + { + "global": 0, + "name": "BL0937 Напряжение", + "type": "Reading", + "subtype": "BL0937v", + "id": "bl_v", + "widget": "anydataVlt", + "page": "BL0937", + "descr": "Напряжение", + "int": 15, + "round": 1 + }, + { + "global": 0, + "name": "BL0937 Сила тока", + "type": "Reading", + "subtype": "BL0937a", + "id": "bl_a", + "widget": "anydataAmp", + "page": "BL0937", + "descr": "Сила тока", + "int": 15, + "round": 1 + }, + { + "global": 0, + "name": "BL0937 Мощность", + "type": "Reading", + "subtype": "BL0937w", + "id": "bl_w", + "widget": "anydataWt", + "page": "BL0937", + "descr": "Мощность", + "int": 15, + "round": 1 + }, + { + "global": 0, + "name": "BL0937 Реакт.Мощность", + "type": "Reading", + "subtype": "BL0937reactw", + "id": "bl_w", + "widget": "anydataWt", + "page": "BL0937", + "descr": "Реакт.Мощность", + "int": 15, + "round": 1 + }, + { + "global": 0, + "name": "BL0937 Активн.Мощность", + "type": "Reading", + "subtype": "BL0937actw", + "id": "bl_actw", + "widget": "anydataWt", + "page": "BL0937", + "descr": "Актив.Мощность", + "int": 15, + "round": 1 + }, + { + "global": 0, + "name": "BL0937 Энергия", + "type": "Reading", + "subtype": "BL0937wh", + "id": "bl_wh", + "widget": "anydataWth", + "page": "BL0937", + "descr": "Энергия", + "int": 15, + "round": 1 + }, + { + "global": 0, + "name": "BL0937 настройка", + "type": "Reading", + "subtype": "BL0937cmd", + "id": "bl_set", + "widget": "nil", + "page": "", + "descr": "", + "btn-reset": "", + "R_current": 0.001, + "R_upstream": 1000000, + "R_downstream": 1000, + "CF_GPIO": 4, + "CF1_GPIO": 5, + "SEL_GPIO": 12, + "expV": 0, + "expA": 0, + "expW": 0 + } + ], + "about": { + "authorName": "Bubnov Mikhail", + "authorContact": "https://t.me/Mit4bmw", + "authorGit": "https://github.com/Mit4el", + "specialThanks": "", + "moduleName": "BL0937", + "moduleVersion": "1.0", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "Счетчик электроэнергии BL0937", + "moduleDesc": "Считает потраченную электроэнергию, измеряет напряжение, силу тока и прочие параметры.", + "propInfo": { + "int": "Количество секунд между опросами датчика.", + "btn-reset": "Энергия BL0937 будет сброшена к нулю.", + "R_current": "Резистор подключенный последовательно к основной линии", + "R_upstream": "это 5 резисторов по 470 Ком в делителе напряжения, который питает вывод V2P", + "R_downstream": "это резистор емкостью 1 Ком в делителе напряжения, который питает вывод V2P", + "CF_GPIO": "пин CF", + "CF1_GPIO": "пин CF1", + "SEL_GPIO": "пин SEL", + "expV": "реальное напряжение, указать для калибровки", + "expA": "реальный ток, указать для калибровки", + "expW": "реальная мощность, указать для калибровки" + } + }, + "defActive": true, + "usedLibs": { + "esp32*": [], + "esp82*": [] + } +} \ No newline at end of file From 9c3ad06c6fab52c1a724afe15da76a0a57a877ff Mon Sep 17 00:00:00 2001 From: Mit4el Date: Tue, 16 Apr 2024 23:36:10 +0300 Subject: [PATCH 22/22] =?UTF-8?q?fix=20=D0=9E=D0=B1=D1=89=D0=B5=D0=B5=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=82=D1=80=D0=B5=D0=B1=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/sensors/EnergyMon485/EnergyMon485.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/modules/sensors/EnergyMon485/EnergyMon485.cpp b/src/modules/sensors/EnergyMon485/EnergyMon485.cpp index c1fbe9b9..113c2765 100644 --- a/src/modules/sensors/EnergyMon485/EnergyMon485.cpp +++ b/src/modules/sensors/EnergyMon485/EnergyMon485.cpp @@ -68,6 +68,7 @@ namespace _Electro485 if (!_myUART_Gran) return 3; unsigned short int param_hex; + mass_to_send[1] = 0x03; mass_to_send[4] = 0x00; if (param == "v") param_hex = 0x0A00; @@ -82,6 +83,7 @@ namespace _Electro485 else if (param == "k") { param_hex = 0x0100; + mass_to_send[1] = 0x04; mass_to_send[4] = 0x01; } else if (param == "p") @@ -90,7 +92,7 @@ namespace _Electro485 } SerialPrint("i", "Gran", "param: " + param + ", param_hex: " + String(param_hex, HEX)); mass_to_send[0] = addr; - mass_to_send[1] = 0x03; + // mass_to_send[1] = 0x03; mass_to_send[2] = param_hex >> 8; mass_to_send[3] = param_hex; @@ -106,7 +108,7 @@ namespace _Electro485 // Serial.println("Count read byte: " + String(s)); // Если вернулся правильный адрес и команда - if (mass_read[0] == addr && mass_read[1] == 0x03) + if (mass_read[0] == addr && mass_read[1] == mass_to_send[1]) { // Проверяем контрольную сумму checkSum = 0;