Merge pull request #381 from Mit4el/ver4dev

Новые модули:
SmartBoiler, Benchmark, BrokerMqtt, Ping, SIM800
Исправления
Исправлена проблема в NTP.cpp високосного года при отображении графиков
Исправления в классе IoTUart

Внимание!
Для модуля src/classes/IoTUart.cpp изменилось поведение функции сценария print(...) - оно теперь не делает пеервод строки в конце - используйте println(...)
This commit is contained in:
2024-04-24 11:00:30 +03:00
committed by GitHub
62 changed files with 7866 additions and 14 deletions

View File

@@ -58,6 +58,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;

View File

@@ -0,0 +1,30 @@
#pragma once
#include <Arduino.h>
#include "Global.h"
#include "classes/IoTItem.h"
#include <map>
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 &parameters);
~IoTBench();
virtual void preLoadFunction();
virtual void postLoadFunction();
virtual void preTaskFunction(const String &id);
virtual void postTaskFunction(const String &id);
protected:
std::map<String, ItemBench *> banchItems;
};

View File

@@ -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();
// делаем доступным модулям отправку сообщений в телеграм

View File

@@ -0,0 +1,20 @@
#pragma once
#include <Arduino.h>
#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"

View File

@@ -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++;
}
};
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,82 @@
#pragma once
#include <Arduino.h>
#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<void()> connected_callback;
std::function<void()> 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;
};
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include <WiFiClient.h>
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);
};
}

View File

@@ -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

View File

@@ -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<void(IncomingPacket & packet)> 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);
}
}
}

View File

@@ -0,0 +1,80 @@
#pragma once
#include <functional>
#include <Arduino.h>
#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<void(IncomingPacket & packet)> 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);
};
}

View File

@@ -0,0 +1,50 @@
#pragma once
#include "config.h"
#ifdef PICOMQTT_DEBUG_TRACE_FUNCTIONS
#include <Arduino.h>
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

View File

@@ -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);
}
}

View File

@@ -0,0 +1,46 @@
#pragma once
#include <Arduino.h>
#include <Client.h>
#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;
};
}

View File

@@ -0,0 +1,224 @@
#include <Client.h>
#include <Print.h>
#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;
}
}
}

View File

@@ -0,0 +1,64 @@
#pragma once
// #define MQTT_OUTGOING_PACKET_DEBUG
#include <Arduino.h>
#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;
};
}

View File

@@ -0,0 +1,49 @@
#pragma once
#include <Arduino.h>
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;
};
}

View File

@@ -0,0 +1,13 @@
#pragma once
namespace PicoMQTT {
class PicoMQTTInterface {
public:
virtual ~PicoMQTTInterface() {}
virtual void begin() {}
virtual void stop() {}
virtual void loop() {}
};
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include <vector>
#include <Arduino.h>
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<Print *> prints;
};
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,90 @@
#pragma once
#include <cstring>
#include <Arduino.h>
#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 <typename TopicStringType>
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 <typename TopicStringType>
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 <typename TopicStringType, typename PayloadStringType>
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 <typename TopicStringType>
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(); }
};
}

View File

@@ -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("<unknown>") {
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<uint8_t> 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);
}
}

View File

@@ -0,0 +1,105 @@
#pragma once
#include <list>
#include <set>
#include <Arduino.h>
#if defined(ESP32)
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#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<Subscription> 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<Client> 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;
};
}

View File

@@ -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<void(char *, void *, size_t)> 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<void(char *, char *)> 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<void(char *)> 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<void(void *, size_t)> callback, size_t max_size) {
TRACE_FUNCTION
return subscribe(topic_filter, [callback](char * topic, void * payload, size_t payload_size) {
callback(payload, payload_size);
});
}
};

View File

@@ -0,0 +1,73 @@
#pragma once
#include <functional>
#include <map>
#include <Arduino.h>
#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<void(char * topic, IncomingPacket & packet)> 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<void(char *, void *, size_t)> callback,
size_t max_size = PICOMQTT_MAX_MESSAGE_SIZE);
SubscriptionId subscribe(const String & topic_filter, std::function<void(char *, char *)> callback,
size_t max_size = PICOMQTT_MAX_MESSAGE_SIZE);
SubscriptionId subscribe(const String & topic_filter, std::function<void(void *, size_t)> callback,
size_t max_size = PICOMQTT_MAX_MESSAGE_SIZE);
SubscriptionId subscribe(const String & topic_filter, std::function<void(char *)> 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<Subscription, MessageCallback> subscriptions;
};
}

View File

@@ -128,6 +128,10 @@
},
"modules": {
"virtual_elments": [
{
"path": "src/modules/virtual/Benchmark",
"active": false
},
{
"path": "src/modules/virtual/Cron",
"active": true
@@ -152,6 +156,10 @@
"path": "src/modules/virtual/owmWeather",
"active": true
},
{
"path": "src/modules/virtual/Ping",
"active": true
},
{
"path": "src/modules/virtual/Timer",
"active": true
@@ -234,6 +242,10 @@
"path": "src/modules/sensors/Emon",
"active": false
},
{
"path": "src/modules/sensors/EnergyMon485",
"active": true
},
{
"path": "src/modules/sensors/ExampleModule",
"active": false
@@ -352,6 +364,10 @@
"path": "src/modules/exec/AnalogBtn",
"active": true
},
{
"path": "src/modules/exec/BrokerMQTT",
"active": false
},
{
"path": "src/modules/exec/ButtonIn",
"active": true
@@ -420,6 +436,14 @@
"path": "src/modules/exec/SDcard",
"active": false
},
{
"path": "src/modules/exec/SIM800",
"active": false
},
{
"path": "src/modules/exec/SmartBoiler",
"active": false
},
{
"path": "src/modules/exec/SysExt",
"active": false

View File

@@ -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}

View File

@@ -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<IoTItem*>::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) {
@@ -58,4 +61,6 @@ void clearConfigure() {
IoTItems.clear();
valuesFlashJson.clear();
benchTaskItem = nullptr;
benchLoadItem = nullptr;
}

View File

@@ -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 = "{}"; // переменная в которой хранятся все ошибки, находится в оперативной памяти только

View File

@@ -2,6 +2,7 @@
#include <time.h>
#include "classes/IoTDB.h"
#include "utils/Statistic.h"
#include "classes/IoTBench.h"
#include <Wire.h>
#if defined(esp32s2_4mb) || defined(esp32s3_16mb)
#include <USB.h>
@@ -16,8 +17,9 @@ String volStrForSave = "";
void elementsLoop() {
// передаем управление каждому элементу конфигурации для выполнения своих функций
for (std::list<IoTItem *>::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 +152,13 @@ void setup() {
iotScen.loadScenario("/scenario.txt");
// создаем событие завершения инициализации основных моментов для возможности выполнения блока кода при загрузке
createItemFromNet("onInit", "1", 1);
elementsLoop();
// elementsLoop(); //Для работы MQTT Брокера перенес ниже, иначе брокер падает если вызван до routerConnect();
stopErrorMarker(SETUPSCEN_ERRORMARKER);
initErrorMarker(SETUPINET_ERRORMARKER);
// подключаемся к роутеру
// подключаемся к роутеру
routerConnect();
// инициализация асинхронного веб сервера и веб сокетов
@@ -179,6 +181,7 @@ void setup() {
initErrorMarker(SETUPLAST_ERRORMARKER);
elementsLoop();
// NTP
ntpInit();
@@ -224,31 +227,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);

View File

@@ -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) {

21
src/classes/IoTBench.cpp Normal file
View File

@@ -0,0 +1,21 @@
#include "Global.h"
#include "classes/IoTBench.h"
IoTBench::IoTBench(const String &parameters) : 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() {}

View File

@@ -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;
}

View File

@@ -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<IoTValue> &param) {
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) {

View File

@@ -0,0 +1,225 @@
#include "Global.h"
#include "classes/IoTItem.h"
#include <Arduino.h>
#include <PicoMQTT.h>
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;
}
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);
}
}
PicoMQTT::ConnectReturnCode auth(const char *client_id, const char *username, const char *password)
{
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;
}
};
myPicoMQTT *picoMqtt = nullptr;
PicoMQTT::Client *clientMqtt = nullptr;
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 (;;)
{
if (clientMqtt)
clientMqtt->loop();
if (picoMqtt)
picoMqtt->loop();
// picoMqtt.loop();
// vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(5));
}
}
class BrokerMQTT : public IoTItem
{
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()
{
static bool flagOne = false;
if (!flagOne)
{
if (!_port)
_port = DEF_PORT;
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, // Функция задачи.
"BrokerMQTT", // Имя задачи.
10000, // Размер стека
NULL, // Параметры задачи
0, // Приоритет
&brokerTask, // Дескриптор задачи для отслеживания
0);
flagOne = true;
}
}
// Основной цикл программы
void loop()
{
IoTItem::loop();
}
~BrokerMQTT()
{
vTaskDelete(brokerTask);
delete picoMqtt;
delete clientMqtt;
}
};
}
void *getAPI_BrokerMQTT(String subtype, String param)
{
if (subtype == F("BrokerMQTT"))
{
return new _Broker::BrokerMQTT(param);
}
else
{
return nullptr;
}
}

View File

@@ -0,0 +1,52 @@
{
"menuSection": "executive_devices",
"configItem": [
{
"global": 0,
"name": "BrokerMQTT",
"type": "Reading",
"subtype": "BrokerMQTT",
"id": "broker",
"widget": "",
"page": "",
"descr": "",
"int": 10,
"value": "",
"port": 1883,
"user": "root",
"pass": "4321",
"brige":1,
"server":"http://iotmanager.org",
"srvUser": "rise",
"srvPass": "3hostel3",
"srvPort": 1883,
"debug": 1
}
],
"about": {
"authorName": "Mikhail Bubnov",
"authorContact": "https://t.me/Mit4bmw",
"authorGit": "https://github.com/Mit4el",
"specialThanks": "Андрей Душин",
"moduleName": "BrokerMQTT",
"moduleVersion": "2.0",
"usedRam": {
"esp32_4mb": 15,
"esp8266_4mb": 15
},
"title": "BrokerMQTT",
"moduleDesc": "MQTT Брокер на основе Pico Mqtt",
"propInfo": {
"port":"Порт, по умолчанию 1883",
"brige":"1 - Использовать режим моста, Брокер будет дублировать все топики в указанные сервер",
"server":"Адрес внешнего MQTT брокера/сервера для режима моста",
"srvUser": "Пользователь внешнего MQTT брокера",
"srvPass": "Пароль внешнего MQTT брокера"
}
},
"defActive": false,
"usedLibs": {
"esp32_4mb3f": [],
"esp32*": []
}
}

View File

@@ -0,0 +1,191 @@
#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"); // переводим в текстовый режим
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)
SerialPrint("I", F("SIM800"), "<- println(" + sms + ")");
// _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')
{
return;
}
if (_inc == '\n')
{
_inStr += "";//_inc;
if (_debug && _inStr != "")
SerialPrint("I", F("SIM800"), "-> " + _inStr);
if (_inStr.indexOf("CPAS") != -1)
{
if (_inStr.indexOf("+CPAS: 0") != -1)
setValue("OK");
else
setValue("NO");
}
_inStr = "";
return;
}
else
_inStr += _inc;
}
}
IoTValue execute(String command, std::vector<IoTValue> &param)
{
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;
}
}

View File

@@ -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*": []
}
}

View File

@@ -0,0 +1,82 @@
#pragma once
#define SLAVE true
#define TIMEOUT_TRESHOLD 5
namespace _Boiler_v2
{
// команды/установки от термостата
struct SetpointBoiler
{
bool cmd_chEnable = 0;
bool 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;
int fault_code = 0;
};
// текущее реальное состояние котла
struct StateBoiler
{
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};
int numStepOn;
} state;
// конфигурация котла
struct ConfigBoiler
{
bool autoPower = true; // если false то управление только из сценария или веба
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;
int numStepDhw;
float maxKW;
} conf;
unsigned long timeout_count = 0;
bool _debug = 0;
bool _telegram = false;
unsigned long ot_response = 0;
int SlaveMemberIDcode = 0;
}

View File

@@ -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<OpenThermMessageType>((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);
}
*/
}

View File

@@ -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 <stdint.h>
#include <Arduino.h>
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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,171 @@
{
"menuSection": "executive_devices",
"configItem": [
{
"global": 0,
"name": "BoilerControl",
"type": "Reading",
"subtype": "BoilerControl",
"id": "boiler",
"widget": "anydataWt",
"page": "Boiler",
"descr": "Котёл",
"int": 1,
"value": "...",
"debug": 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",
"changeRele":0,
"idRelePump": "relePump",
"minCH": 35,
"maxCH": 85,
"gistCH": 5,
"antiFreez":10,
"maxKW": 24
},
{
"global": 0,
"name": "DHWControl",
"type": "Reading",
"subtype": "DHWControl",
"id": "dhw",
"widget": "",
"page": "Boiler",
"descr": "Котёл",
"int": 1,
"value": "...",
"idTdhw": "TDhw",
"idReleDhw": "ReleDhw",
"idCmdDHW":"CmdDHW",
"idStateDHW":"StateDHW",
"idSetDHW":"SetDHW",
"minDhw": 20,
"maxDhw": 60,
"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": {
"authorName": "Mikhail Bubnov",
"authorContact": "https://t.me/Mit4bmw",
"authorGit": "https://github.com/Mit4el",
"specialThanks": "",
"moduleName": "SmartBoiler",
"moduleVersion": "2.0",
"usedRam": {
"esp32_4mb": 15,
"esp8266_4mb": 15
},
"subTypes": [
"BoilerControl",
"OpenThermSlave",
"DHWControl"
],
"title": "SmartBoiler",
"moduleDesc": "Модуль для автоматизации электрического котла. Мозги котла с внешним протоколом opentherm. Модуль OpenThermSlave_v2 id модулй использует теже, что указаны в BoilerControl_v2. Но так же может работать автономно, если нет модуля BoilerControl_v2, он ищет модули по ID по умолчаию",
"propInfo": {
"int": "Интервал обработки логики и опроса внешних модулей",
"telegram": "1- Будет отправлять в телеграмм оповещения при ошибках котла и пропаже сигнала от котла, остальные необходимо реализовывать через сценарий",
"MemberID": "SlaveMemberIDcode - код производителя котла, кем притворится котёл;) Менять в большинстве случаев не надо",
"idPID":"ID модуля ПИД регулятора, для расчета модуляции и включения тэнов в зависимости от температуры теплоносителя, в модуле TCHSet будет уставка СО, создать TCHSet и указать его в модуле ПИД",
"idTboiler": "ID датчика температуры подачи котла",
"idTret": "ID датчика температуры обратки котла, только для передачи по opentherm",
"idToutside": "ID датчика уличной температуры, только для передачи по opentherm",
"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гр от указанной",
"maxKW": "Максимальная мощность котла при включении на поеднем Шаге Мощности",
"numStepDhw":"На каком Шаге Мощности включать ГВС"
},
"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) - выкл "
]
},
{
"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()"
]
}
]
},
"defActive": false,
"usedLibs": {
"esp32_4mb3f": [],
"esp32*": [],
"esp82*": []
}
}

View File

@@ -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 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, строкой: "не подключен" / "подключен"

View File

@@ -0,0 +1,237 @@
{
"mark": "iotm",
"config": [
{
"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": "anydataHum",
"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": "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,
"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": "boiler",
"widget": "anydataWt",
"page": "Котёл",
"descr": "Котёл",
"int": 1,
"value": "...",
"debug": "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",
"changeRele": 0,
"idRelePump": "relePump",
"minCH": 35,
"maxCH": 85,
"gistCH": 5,
"antiFreez": 10,
"maxKW": "8"
}
]
}
scenario=>if onStart then
{
boiler.addStepPower(1, 3, "rele1");
boiler.addStepPower(2, 5, "rele2", "rele3");
boiler.addStepPower(3, 8, "rele1", "rele2", "rele3");
#boiler.onStepPower(2);
#boiler.autoPower();
}

View File

@@ -110,8 +110,13 @@ class Telegram : public IoTItem {
}
}
~Telegram() {
IoTItem *getTlgrmDriver()
{
return this;
}
~Telegram() {
tlgrmItem = nullptr;
};
};

View File

@@ -58,7 +58,14 @@ class TelegramLT : public IoTItem {
return {};
}
~TelegramLT(){};
IoTItem *getTlgrmDriver()
{
return this;
}
~TelegramLT() {
tlgrmItem = nullptr;
};
};
void *getAPI_TelegramLT(String subtype, String param) {

View File

@@ -0,0 +1,269 @@
#include "Global.h"
#include "classes/IoTItem.h"
#include "BL0937lib.h"
// #include "classes/IoTUart.h"
// #include <map>
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<IoTValue> &param)
{
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;
}
}

View File

@@ -0,0 +1,238 @@
/*
BL0937
Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
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 <http://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#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;
}

View File

@@ -0,0 +1,146 @@
/*
BL0937
Copyright (C) 2016-2018 by Xose Pérez <xose dot perez at gmail dot com>
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 <http://www.gnu.org/licenses/>.
*/
#ifndef BL0937_h
#define BL0937_h
#include <Arduino.h>
// 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

View File

@@ -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*": []
}
}

View File

@@ -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};
}

View File

@@ -0,0 +1,342 @@
#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 &param, float &res)
{
int8_t ret;
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;
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[1] = 0x04;
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] == mass_to_send[1])
{
// Проверяем контрольную сумму
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 &param, 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;
}
}

View File

@@ -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": []
}
}

View File

@@ -0,0 +1,165 @@
#include "Global.h"
#include "classes/IoTBench.h"
#include <ArduinoJson.h>
// #include <map>
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;
}
}

View File

@@ -0,0 +1,57 @@
{
"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": false,
"usedLibs": {
"esp32*": [],
"esp82*": []
}
}

View File

@@ -0,0 +1,240 @@
#include "Global.h"
#include "classes/IoTItem.h"
#include <ArduinoJson.h>
#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 <ESP8266Ping.h>
#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; //Есть только в новых версиях framwork ПОКА УБРАЛ
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", "<t>", "Time to wait for a response, in seconds");
ping_args.interval = 1; // arg_dbl0("i", "interval", "<t>", "Wait interval seconds between sending each packet");
ping_args.data_size = 0; // arg_int0("s", "size", "<n>", "Specify the number of data bytes to be sent");
ping_args.count = 0; // arg_int0("c", "count", "<n>", "Stop after sending count packets");
ping_args.tos = 0; // arg_int0("Q", "tos", "<n>", "Set Type of Service related bits in IP datagrams");
ping_args.ttl = 0; // arg_int0("T", "ttl", "<n>", "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<IoTValue> &param)
{
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.valD, "ping");
return val;
}
#endif
}
return {};
}
~PingIoTM(){};
};
void *getAPI_Ping(String subtype, String param)
{
if (subtype == F("Ping"))
{
return new PingIoTM(param);
}
else
{
return nullptr;
}
}

View File

@@ -0,0 +1,66 @@
{
"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
}
],
"about": {
"authorName": "Mikhail Bubnov",
"authorContact": "https://t.me/Mit4bmw",
"authorGit": "https://github.com/Mit4el",
"specialThanks": "",
"moduleName": "Ping",
"moduleVersion": "1.2",
"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, то пингует со значением по умолчанию"
},
"funcInfo": [
{
"name": "ping",
"descr": "Проверить пинг. после вызова данной функции из сценария результат будет в значении самого модуля. if ping21==1 then ЕСТЬ_пинг else НЕТ_пинга",
"params": [
"IP адрес или имя хоста"
]
}
]
},
"defActive": true,
"usedLibs": {
"esp32*": [
],
"esp82*": [
"https://github.com/dancol90/ESP8266Ping"
]
}
}

View File

@@ -0,0 +1,52 @@
{
"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
}
]
}
scenario=>if vbtn67==1 then ping21.ping("8.8.8.8")
if ping21==0 then vout96 = "нет интернета" else vout96 ="есть интернет"

27
tools/patch32_ws.py Normal file
View File

@@ -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)