Merge branch 'v2a8' of https://github.com/proddy/EMS-ESP into v2

This commit is contained in:
MichaelDvP
2020-05-28 08:25:36 +02:00
36 changed files with 857 additions and 532 deletions

View File

@@ -41,7 +41,7 @@ Note: Version 2.0 is not backward compatible with v1.0. The File system structur
common commands available in all contexts:
exit
help
log [level] [trace ID]
log [level] [raw] [trace ID]
su
(top level)
@@ -124,9 +124,9 @@ thermostat
### **Known issues, bugs and improvements currently working on**
```
TODO when doing show, should we sort the ems devices?
TODO ESP32 - when saving SPIFFS the UART stop and restart() functions need to flush queue to avoid miss fires
TODO network issues with ESP8266 - can take a while to get an IP address. DNS issue?
TODO figure out why sometimes telnet on ESP32 (and sometimes ESP8266) has slow response times. After a manual reset it seems to fix itself. Perhaps the telnet service needs to start after the wifi is up & running.
TODO Get the ESP32 UART code working.
TODO sometimes with tx_mode 0 there are a few CRC errors due to collision when waiting for a BRK signal.
TODO console auto-complete with 'set' command in the system context is not showing all commands, only the hostname.
```
@@ -152,4 +152,6 @@ TODO See if it's easier to use timers instead of millis() timers, using https://
```
TODO merge in the web code which has the Captive AP and better wifi reconnect logic. Use IPV6 and NTP from lwip2
TODO decide if I want to port over the shower one-shot cold water logic. Don't think its used.
TODO when doing show in telnet, should we sort the ems devices?
```

View File

@@ -26,15 +26,25 @@ uint64_t get_uptime_ms() {
static uint32_t high_millis = 0;
static uint32_t low_millis = 0;
uint32_t now_millis = ::millis();
if (now_millis < low_millis) {
if (get_uptime() < low_millis) {
high_millis++;
}
low_millis = now_millis;
low_millis = get_uptime();
return ((uint64_t)high_millis << 32) | low_millis;
}
// added by proddy
static uint32_t now_millis; // added by proddy
void set_uptime() {
now_millis = ::millis();
}
uint32_t get_uptime() {
return now_millis;
}
} // namespace uuid

View File

@@ -21,6 +21,7 @@
namespace uuid {
void loop() {
set_uptime(); // added by proddy
get_uptime_ms();
}

View File

@@ -86,6 +86,9 @@ void loop();
*/
uint64_t get_uptime_ms();
uint32_t get_uptime(); // added by proddy
void set_uptime();
} // namespace uuid
#endif

View File

@@ -2,9 +2,9 @@
; For EMS-ESP
[platformio]
;default_envs = esp8266
default_envs = esp8266
; default_envs = esp32
default_envs = esp32_d1
;default_envs = esp32_d1
# override any settings with your own local ones in pio_local.ini
extra_configs = pio_local.ini
@@ -34,7 +34,7 @@ build_flags = -std=c++11 -Os -fno-exceptions
-D ARDUINOJSON_USE_LONG_LONG=0
-D BEARSSL_SSL_BASIC
-D PROGMEM_WWW
-D UUID_TELNET_HAVE_WIFICLIENT_NODELAY=0
-D UUID_TELNET_HAVE_WIFICLIENT_NODELAY=1
libs_core =
ArduinoJson
@@ -59,18 +59,18 @@ check_flags =
clangtidy: --checks=-*,clang-analyzer-*,performance-*
; USB upload
upload_protocol = esptool
; upload_protocol = esptool
; example ports for OSX
;upload_port = /dev/cu.wchusbserial14403
;upload_port = /dev/cu.usbserial-1440
;upload_port = /dev/cu.SLAB_USBtoUART
; OTA upload
; upload_protocol = espota
; upload_flags =
; --port=8266
; --auth=neo
; upload_port = ems-esp.local
upload_protocol = espota
upload_flags =
--port=8266
--auth=neo
upload_port = 192.168.0.20
[env:esp8266]
build_type = release

View File

@@ -46,7 +46,7 @@ uuid::log::Logger Boiler::logger_{F_(logger_name), uuid::log::Facility::CONSOLE}
Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
DEBUG_LOG(F("Registering new Boiler with device ID 0x%02X"), device_id);
LOG_DEBUG(F("Registering new Boiler with device ID 0x%02X"), device_id);
// the telegram handlers...
register_telegram_type(0x18, F("UBAMonitorFast"), true, std::bind(&Boiler::process_UBAMonitorFast, this, _1));
@@ -91,7 +91,7 @@ void Boiler::boiler_cmd(const char * message) {
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
DeserializationError error = deserializeJson(doc, message);
if (error) {
DEBUG_LOG(F("MQTT error: payload %s, error %s"), message, error.c_str());
LOG_DEBUG(F("MQTT error: payload %s, error %s"), message, error.c_str());
return;
}
const char * command = doc["cmd"];
@@ -312,7 +312,7 @@ void Boiler::publish_values() {
}
#ifdef EMSESP_DEBUG
DEBUG_LOG(F("[DEBUG] Performing a boiler publish"));
LOG_DEBUG(F("[DEBUG] Performing a boiler publish"));
#endif
// if we have data, publish it
@@ -675,31 +675,31 @@ void Boiler::process_UBAMaintenanceSettings(std::shared_ptr<const Telegram> tele
// Set the warm water temperature 0x33
void Boiler::set_warmwater_temp(const uint8_t temperature) {
logger_.info(F("Setting boiler warm water temperature to %d C"), temperature);
LOG_INFO(F("Setting boiler warm water temperature to %d C"), temperature);
write_command(EMS_TYPE_UBAParameterWW, 2, temperature);
}
// flow temp
void Boiler::set_flow_temp(const uint8_t temperature) {
logger_.info(F("Setting boiler flow temperature to %d C"), temperature);
LOG_INFO(F("Setting boiler flow temperature to %d C"), temperature);
write_command(EMS_TYPE_UBASetPoints, 0, temperature);
}
// 1=hot, 2=eco, 3=intelligent
void Boiler::set_warmwater_mode(const uint8_t comfort) {
if (comfort == 1) {
logger_.info(F("Setting boiler warm water to hot"));
LOG_INFO(F("Setting boiler warm water to hot"));
} else if (comfort == 2) {
logger_.info(F("Setting boiler warm water to eco"));
LOG_INFO(F("Setting boiler warm water to eco"));
} else if (comfort == 3) {
logger_.info(F("Setting boiler warm water to intelligent"));
LOG_INFO(F("Setting boiler warm water to intelligent"));
}
write_command(EMS_TYPE_UBAParameterWW, 9, comfort);
}
// turn on/off warm water
void Boiler::set_warmwater_activated(const bool activated) {
logger_.info(F("Setting boiler warm water %s"), activated ? "on" : "off");
LOG_INFO(F("Setting boiler warm water %s"), activated ? "on" : "off");
uint8_t value;
// https://github.com/proddy/EMS-ESP/issues/268
@@ -715,7 +715,7 @@ void Boiler::set_warmwater_activated(const bool activated) {
// true = on, false = off
// Note: Using the type 0x1D to put the boiler into Test mode. This may be shown on the boiler with a flashing 'T'
void Boiler::set_tapwarmwater_activated(const bool activated) {
logger_.info(F("Setting boiler warm tap water %s"), activated ? "on" : "off");
LOG_INFO(F("Setting boiler warm tap water %s"), activated ? "on" : "off");
uint8_t message_data[EMS_MAX_TELEGRAM_MESSAGE_LENGTH];
for (uint8_t i = 0; i < sizeof(message_data); i++) {
message_data[i] = 0x00;
@@ -743,14 +743,14 @@ void Boiler::set_tapwarmwater_activated(const bool activated) {
// true = on, false = off
// See also https://github.com/proddy/EMS-ESP/issues/341#issuecomment-596245458 for Junkers
void Boiler::set_warmwater_onetime(const bool activated) {
logger_.info(F("Setting boiler warm water OneTime loading %s"), activated ? "on" : "off");
LOG_INFO(F("Setting boiler warm water OneTime loading %s"), activated ? "on" : "off");
write_command(EMS_TYPE_UBAFlags, 0, (activated ? 0x22 : 0x02));
}
// Activate / De-activate circulation of warm water 0x35
// true = on, false = off
void Boiler::set_warmwater_circulation(const bool activated) {
logger_.info(F("Setting boiler warm water circulation %s"), activated ? "on" : "off");
LOG_INFO(F("Setting boiler warm water circulation %s"), activated ? "on" : "off");
write_command(EMS_TYPE_UBAFlags, 1, (activated ? 0x22 : 0x02));
}

View File

@@ -221,8 +221,9 @@ void Console::load_standard_commands(unsigned int context) {
context,
CommandFlags::USER,
flash_string_vector{F_(log)},
flash_string_vector{F_(log_level_optional), F_(traceid_optional)},
flash_string_vector{F_(log_level_optional), F_(trace_format_optional), F_(traceid_optional)},
[](Shell & shell, const std::vector<std::string> & arguments) {
uint16_t watch_id;
if (!arguments.empty()) {
uuid::log::Level level;
@@ -233,15 +234,34 @@ void Console::load_standard_commands(unsigned int context) {
return;
}
// see if we have extra argument, for trace
uint16_t watch_id = 0; // no watch ID set
if ((arguments.size() == 2) && (level == uuid::log::Level::TRACE)) {
watch_id = Helpers::hextoint(arguments[1].c_str());
shell.printfln(("Tracing only telegrams that match a device ID or telegram type of 0x%02X"), watch_id);
// trace logic
if (level == uuid::log::Level::TRACE) {
watch_id = LOG_TRACE_WATCH_NONE; // no watch ID set
if (arguments.size() > 1) {
// next argument is raw or full
if (arguments[1] == read_flash_string(F_(raw))) {
emsesp::EMSESP::trace_raw(true);
} else if (arguments[1] == read_flash_string(F_(full))) {
emsesp::EMSESP::trace_raw(false);
}
emsesp::EMSESP::trace_watch_id(watch_id);
// get the watch_id if its set
if (arguments.size() == 3) {
emsesp::EMSESP::trace_watch_id(Helpers::hextoint(arguments[2].c_str()));
}
}
}
}
// print out logging settings
shell.printfln(F_(log_level_fmt), uuid::log::format_level_uppercase(shell.log_level()));
watch_id = emsesp::EMSESP::trace_watch_id();
if (watch_id == LOG_TRACE_WATCH_NONE) {
shell.printfln(F("Tracing all telegrams"));
} else {
shell.printfln(F("Tracing only telegrams that match a device ID or telegram type of 0x%02X"), watch_id);
}
shell.printfln(F_(trace_raw_fmt), emsesp::EMSESP::trace_raw() ? uuid::read_flash_string(F_(on)).c_str() : uuid::read_flash_string(F_(off)).c_str());
},
[](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments __attribute__((unused))) -> std::vector<std::string> {
return uuid::log::levels_lowercase();
@@ -384,6 +404,8 @@ EMSESPStreamConsole::~EMSESPStreamConsole() {
#endif
ptys_[pty_] = false;
ptys_.shrink_to_fit();
EMSESPShell::commands->remove_all_commands();
}
}
@@ -414,7 +436,7 @@ void Console::start() {
// note, this must be started after the network/wifi for ESP32 otherwise it'll crash
#ifndef EMSESP_STANDALONE
telnet_.start();
telnet_.default_write_timeout(1000); // in ms, socket timeout 1 second
// telnet_.default_write_timeout(1000); // in ms, socket timeout 1 second
#endif
}

View File

@@ -41,8 +41,12 @@ using uuid::log::Level;
// clang-format off
#define DEBUG_LOG(...) if (logger_.enabled(Level::DEBUG)) {logger_.debug(__VA_ARGS__);}
#define TRACE_LOG(...) if (logger_.enabled(Level::TRACE)) {logger_.trace(__VA_ARGS__);}
#define LOG_DEBUG(...) if (logger_.enabled(Level::DEBUG)) {logger_.debug(__VA_ARGS__);}
#define LOG_TRACE(...) if (logger_.enabled(Level::TRACE)) {logger_.trace(__VA_ARGS__);}
#define LOG_INFO(...) logger_.info(__VA_ARGS__)
#define LOG_NOTICE(...) logger_.notice(__VA_ARGS__)
#define LOG_WARNING(...) logger_.warning(__VA_ARGS__)
#define LOG_ERROR(...) logger_.err(__VA_ARGS__)
#define MAKE_PSTR(string_name, string_literal) static const char __pstr__##string_name[] __attribute__((__aligned__(sizeof(int)))) PROGMEM = string_literal;
#define MAKE_PSTR_WORD(string_name) MAKE_PSTR(string_name, #string_name)
@@ -84,6 +88,8 @@ MAKE_PSTR_WORD(debug)
MAKE_PSTR_WORD(restart)
MAKE_PSTR_WORD(reconnect)
MAKE_PSTR_WORD(format)
MAKE_PSTR_WORD(raw)
MAKE_PSTR_WORD(full)
// context menus
MAKE_PSTR_WORD(mqtt)
@@ -96,6 +102,8 @@ MAKE_PSTR(asterisks, "********")
MAKE_PSTR(n_mandatory, "<n>")
MAKE_PSTR(n_optional, "[n]")
MAKE_PSTR(traceid_optional, "[trace ID]")
MAKE_PSTR(trace_raw_fmt, "Displaying raw bytes = %s")
MAKE_PSTR(trace_format_optional, "[full|raw]")
MAKE_PSTR(bool_mandatory, "<on|off>")
MAKE_PSTR(typeid_mandatory, "<type ID>")
MAKE_PSTR(deviceid_mandatory, "<device ID>")

View File

@@ -37,7 +37,6 @@
{122, DeviceType::BOILER, F("Proline"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{170, DeviceType::BOILER, F("Logano GB212"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{172, DeviceType::BOILER, F("Enviline/Compress 6000AW"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{ 72, DeviceType::BOILER, F("MC10"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
// Solar Modules - 0x30
{ 73, DeviceType::SOLAR, F("SM10"), DeviceFlags::EMS_DEVICE_FLAG_SM10},

View File

@@ -160,7 +160,7 @@ void EMSdevice::show_values(uuid::console::Shell & shell) {
// for each telegram that has the fetch value set (true) do a read request
void EMSdevice::fetch_values() {
DEBUG_LOG(F("Fetching values for device ID 0x%02X"), device_id());
LOG_DEBUG(F("Fetching values for device ID 0x%02X"), device_id());
for (const auto & tf : telegram_functions_) {
if (tf.fetch_) {
@@ -188,7 +188,7 @@ void EMSdevice::show_mqtt_handlers(uuid::console::Shell & shell) {
}
void EMSdevice::register_mqtt_topic(const std::string & topic, mqtt_function_p f) {
DEBUG_LOG(F("Registering MQTT topic %s for device ID %02X"), topic.c_str(), this->device_id_);
LOG_DEBUG(F("Registering MQTT topic %s for device ID %02X"), topic.c_str(), this->device_id_);
Mqtt::subscribe(this->device_id_, topic, f);
}
@@ -230,7 +230,7 @@ std::string EMSdevice::telegram_type_name(std::shared_ptr<const Telegram> telegr
bool EMSdevice::process_telegram(std::shared_ptr<const Telegram> telegram) {
for (const auto & tf : telegram_functions_) {
if (tf.telegram_type_id_ == telegram->type_id) {
DEBUG_LOG(F("Processing %s..."), uuid::read_flash_string(tf.telegram_type_name_).c_str());
LOG_DEBUG(F("Processing %s..."), uuid::read_flash_string(tf.telegram_type_name_).c_str());
tf.process_function_(telegram);
return true;
}

View File

@@ -56,7 +56,8 @@ Shower EMSESP::shower_; // Shower logic
// static/common variables
uint8_t EMSESP::actual_master_thermostat_ = EMSESP_DEFAULT_MASTER_THERMOSTAT; // which thermostat leads when multiple found
uint16_t EMSESP::trace_watch_id_ = 0; // for when log is TRACE
uint16_t EMSESP::trace_watch_id_ = LOG_TRACE_WATCH_NONE; // for when log is TRACE. 0 means no trace set
bool EMSESP::trace_raw_ = false; // not showing raw when in trace logging
bool EMSESP::tap_water_active_ = false; // for when Boiler states we having running warm water. used in Shower()
bool EMSESP::ems_read_only_;
uint32_t EMSESP::last_fetch_ = 0;
@@ -332,7 +333,7 @@ void EMSESP::process_UBADevices(std::shared_ptr<const Telegram> telegram) {
// if we haven't already detected this device, request it's version details, unless its us (EMS-ESP)
// when the version info is received, it will automagically add the device
if ((device_id != ems_bus_id) && !(EMSESP::device_exists(device_id))) {
logger_.info(F("New EMS device detected with ID 0x%02X. Requesting version information."), device_id);
LOG_DEBUG(F("New EMS device detected with ID 0x%02X. Requesting version information."), device_id);
send_read_request(EMSdevice::EMS_TYPE_VERSION, device_id);
}
}
@@ -391,9 +392,11 @@ void EMSESP::process_version(std::shared_ptr<const Telegram> telegram) {
// We also check for common telgram types, like the Version(0x02)
// returns false if there are none found
bool EMSESP::process_telegram(std::shared_ptr<const Telegram> telegram) {
// print to console if logging is TRACE
if ((trace_watch_id_ == 0) || (telegram->src == trace_watch_id_) || (telegram->dest == trace_watch_id_) || (telegram->type_id == trace_watch_id_)) {
TRACE_LOG(pretty_telegram(telegram).c_str());
if ((logger_.enabled(Level::TRACE)) && !trace_raw()) {
if ((trace_watch_id_ == LOG_TRACE_WATCH_NONE) || (telegram->src == trace_watch_id_) || (telegram->dest == trace_watch_id_)
|| (telegram->type_id == trace_watch_id_)) {
LOG_TRACE(pretty_telegram(telegram).c_str());
}
}
// only process broadcast telegrams or ones sent to us on request
@@ -431,7 +434,7 @@ bool EMSESP::process_telegram(std::shared_ptr<const Telegram> telegram) {
}
if (!found) {
DEBUG_LOG(F("No telegram type handler found for type ID 0x%02X (src 0x%02X, dest 0x%02X)"), telegram->type_id, telegram->src, telegram->dest);
LOG_DEBUG(F("No telegram type handler found for type ID 0x%02X (src 0x%02X, dest 0x%02X)"), telegram->type_id, telegram->src, telegram->dest);
}
return found;
@@ -504,7 +507,7 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std::
for (const auto & emsdevice : emsdevices) {
if (emsdevice) {
if (emsdevice->is_device_id(device_id)) {
DEBUG_LOG(F("Updating details for already existing device with ID 0x%02X"), device_id);
LOG_DEBUG(F("Updating details for already existing device with ID 0x%02X"), device_id);
emsdevice->product_id(product_id);
emsdevice->version(version);
emsdevice->brand(brand);
@@ -535,10 +538,10 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std::
// if we don't recognize the product ID report it, but don't add it.
if (!found) {
logger_.notice(F("Unrecognized EMS device with device ID 0x%02X with product ID %d. Please report on GitHub."), device_id, product_id);
LOG_NOTICE(F("Unrecognized EMS device with device ID 0x%02X with product ID %d. Please report on GitHub."), device_id, product_id);
return false; // not found
} else {
DEBUG_LOG(F("Adding new device with device ID 0x%02X with product ID %d"), device_id, product_id);
LOG_DEBUG(F("Adding new device with device ID 0x%02X with product ID %d"), device_id, product_id);
// go and fetch its data, including asking for the version
send_read_request(EMSdevice::EMS_TYPE_VERSION, device_id);
fetch_device_values(device_id);
@@ -569,7 +572,6 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
// check first for echo
uint8_t first_value = data[0];
if (((first_value & 0x7F) == txservice_.ems_bus_id()) && (length > 1)) {
DEBUG_LOG(F("Tx echo"));
return; // it's an echo
}
@@ -579,12 +581,12 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
EMSbus::tx_waiting(false); // reset Tx wait state
if (length == 1) {
if (first_value == TxService::TX_WRITE_SUCCESS) {
DEBUG_LOG(F("Last Tx write successful. Sending read request."));
LOG_DEBUG(F("Last Tx write successful. Sending read request."));
txservice_.increment_telegram_write_count(); // last tx/write was confirmed ok
txservice_.send_poll(); // close the bus
txservice_.post_send_query(); // send type_id to last destination
} else if (first_value == TxService::TX_WRITE_FAIL) {
DEBUG_LOG(F("Last Tx write rejected by host"));
LOG_DEBUG(F("Last Tx write rejected by host"));
txservice_.send_poll(); // close the bus
} else {
// ignore it, it's probably a poll and we can wait for the next one
@@ -596,7 +598,7 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
uint8_t src = data[0];
uint8_t dest = data[1];
if (txservice_.is_last_tx(src, dest)) {
DEBUG_LOG(F("Last Tx read successful"));
LOG_DEBUG(F("Last Tx read successful"));
txservice_.increment_telegram_read_count();
txservice_.send_poll();
} else {
@@ -604,9 +606,9 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
// So re-send the last Tx and increment retry count
uint8_t retries = txservice_.retry_tx(); // returns 0 if exceeded count
if (retries) {
DEBUG_LOG(F("Last Tx read failed. Retrying #%d..."), retries);
LOG_DEBUG(F("Last Tx read failed. Retrying #%d..."), retries);
} else {
DEBUG_LOG(F("Last Tx read failed. Giving up"));
LOG_DEBUG(F("Last Tx read failed. Giving up"));
}
}
}
@@ -617,7 +619,7 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
// check for poll to us, if so send top message from Tx queue immediately and quit
// if ht3 poll must be ems_bus_id else if Buderus poll must be (ems_bus_id | 0x80)
if ((first_value ^ 0x80 ^ rxservice_.ems_mask()) == txservice_.ems_bus_id()) {
EMSbus::last_bus_activity(millis()); // set the flag indication the EMS bus is active
EMSbus::last_bus_activity(uuid::get_uptime()); // set the flag indication the EMS bus is active
txservice_.send();
}
return;
@@ -635,7 +637,7 @@ void EMSESP::send_raw_telegram(const char * data) {
// sets the ems read only flag preventing any Tx from going out
void EMSESP::set_ems_read_only() {
ems_read_only_ = Settings().ems_read_only();
DEBUG_LOG(F("Setting EMS read-only mode to %s"), ems_read_only_ ? F("on") : F("off"));
LOG_DEBUG(F("Setting EMS read-only mode to %s"), ems_read_only_ ? F("on") : F("off"));
}
// console commands to add
@@ -693,17 +695,17 @@ void EMSESP::console_commands(Shell & shell, unsigned int context) {
flash_string_vector{F_(n_mandatory)},
[](Shell & shell, const std::vector<std::string> & arguments) {
uint8_t tx_mode = (arguments[0]).at(0) - '0';
if ((tx_mode > 0) && (tx_mode <= 3)) {
if ((tx_mode > 0) && (tx_mode <= 4)) {
Settings settings;
settings.ems_tx_mode(tx_mode);
settings.commit();
shell.printfln(F_(tx_mode_fmt), settings.ems_tx_mode());
} else {
shell.println(F("Must be 1 for EMS generic, 2 for EMS+ or 3 for HT3"));
shell.println(F("Must be 1 for EMS generic, 2 for EMS+, 3 for HT3, 4 for experimental"));
}
},
[](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments __attribute__((unused))) -> const std::vector<std::string> {
return std::vector<std::string>{read_flash_string(F("1")), read_flash_string(F("2")), read_flash_string(F("3"))};
return std::vector<std::string>{read_flash_string(F("1")), read_flash_string(F("2")), read_flash_string(F("3")), read_flash_string(F("4"))};
});
EMSESPShell::commands->add_command(
@@ -787,7 +789,7 @@ void EMSESP::console_commands(Shell & shell, unsigned int context) {
// kick off the party, start all the services
void EMSESP::start() {
// Load our libary of known devices
// Load our library of known devices
device_library_ = {
#include "device_library.h"
};
@@ -799,25 +801,27 @@ void EMSESP::start() {
rxservice_.start();
txservice_.start();
shower_.start();
mqtt_.start();
set_ems_read_only(); // see if we have Tx disabled and set the flag
}
// loop de loop
void EMSESP::loop() {
// network returns false if an OTA is being carried out.
// network returns false if an OTA is being carried out
// so we disable all services when an OTA is happening
if (network_.loop()) {
console_.loop(); // telnet/serial console
system_.loop(); // does LED and checks system health, and syslog service
mqtt_.loop(); // starts mqtt, and sends out anything in the queue
rxservice_.loop(); // process what ever is in the rx queue
txservice_.loop(); // check that the Tx is all ok
shower_.loop(); // check for shower on/off
sensors_.loop(); // this will also send out via MQTT
// force a query on the EMS devices to fetch latest data
uint32_t currentMillis = millis();
if ((currentMillis - last_fetch_ > EMS_FETCH_FREQUENCY)) {
last_fetch_ = currentMillis;
// force a query on the EMS devices to fetch latest data at a set interval (1 min)
if ((uuid::get_uptime() - last_fetch_ > EMS_FETCH_FREQUENCY)) {
last_fetch_ = uuid::get_uptime();
fetch_device_values();
}
}

View File

@@ -45,6 +45,8 @@
#include "boiler.h"
#include "shower.h"
#define LOG_TRACE_WATCH_NONE 0 // no watch set
namespace emsesp {
class Shower; // forward declaration for compiler
@@ -79,8 +81,6 @@ class EMSESP {
static uint8_t actual_master_thermostat();
static void actual_master_thermostat(const uint8_t device_id);
static void trace_watch_id(uint16_t);
static void show_values(uuid::console::Shell & shell);
static void show_devices(uuid::console::Shell & shell);
static void show_emsbus(uuid::console::Shell & shell);
@@ -93,10 +93,20 @@ class EMSESP {
return sensors_.devices();
}
static void trace_watch_id(uint16_t id);
static uint16_t trace_watch_id() {
return trace_watch_id_;
}
static void trace_raw(bool trace_raw) {
trace_raw_ = trace_raw;
}
static bool trace_raw() {
return trace_raw_;
}
static bool tap_water_active() {
return tap_water_active_;
}
@@ -151,6 +161,7 @@ class EMSESP {
static uint8_t actual_master_thermostat_;
static uint16_t trace_watch_id_;
static bool trace_raw_;
static bool tap_water_active_;
static bool ems_read_only_;
};

View File

@@ -35,7 +35,7 @@ uuid::log::Logger Heatpump::logger_{F_(logger_name), uuid::log::Facility::CONSOL
Heatpump::Heatpump(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
DEBUG_LOG(F("Registering new Heat Pump module with device ID 0x%02X"), device_id);
LOG_DEBUG(F("Registering new Heat Pump module with device ID 0x%02X"), device_id);
// telegram handlers
register_telegram_type(0x047B, F("HP1"), true, std::bind(&Heatpump::process_HPMonitor1, this, _1));

View File

@@ -265,7 +265,8 @@ std::string Helpers::data_to_hex(const uint8_t * data, const uint8_t length) {
}
// takes a hex string and convert it to a 32bit number (max 8 hex digits)
// takes a hex string and convert it to an unsigned 32bit number (max 8 hex digits)
// works with only positive numbers
uint32_t Helpers::hextoint(const char * hex) {
uint32_t val = 0;
while (*hex) {

View File

@@ -26,20 +26,25 @@ uuid::log::Logger Mixing::logger_{F_(logger_name), uuid::log::Facility::CONSOLE}
Mixing::Mixing(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
DEBUG_LOG(F("Registering new Mixing module with device ID 0x%02X"), device_id);
LOG_DEBUG(F("Registering new Mixing module with device ID 0x%02X"), device_id);
if (flags == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) {
if (device_id < 0x28) {
// telegram handlers 0x20 - 0x27 for HC
register_telegram_type(0x02D7, F("MMPLUSStatusMessage_HC"), true, std::bind(&Mixing::process_MMPLUSStatusMessage_HC, this, _1));
register_telegram_type(0x02D8, F("MMPLUSStatusMessage_HC"), true, std::bind(&Mixing::process_MMPLUSStatusMessage_HC, this, _1));
register_telegram_type(0x02D9, F("MMPLUSStatusMessage_HC"), true, std::bind(&Mixing::process_MMPLUSStatusMessage_HC, this, _1));
register_telegram_type(0x02DA, F("MMPLUSStatusMessage_HC"), true, std::bind(&Mixing::process_MMPLUSStatusMessage_HC, this, _1));
register_telegram_type(device_id -0x20 + 0x02D7, F("MMPLUSStatusMessage_HC"), true, std::bind(&Mixing::process_MMPLUSStatusMessage_HC, this, _1));
} else {
// telegram handlers for warm water/DHW 0x28, 0x29
register_telegram_type(0x0331, F("MMPLUSStatusMessage_WWC"), true, std::bind(&Mixing::process_MMPLUSStatusMessage_WWC, this, _1));
register_telegram_type(0x0332, F("MMPLUSStatusMessage_WWC"), true, std::bind(&Mixing::process_MMPLUSStatusMessage_WWC, this, _1));
register_telegram_type(device_id - 0x28 + 0x0331, F("MMPLUSStatusMessage_WWC"), true, std::bind(&Mixing::process_MMPLUSStatusMessage_WWC, this, _1));
}
}
// EMS 1.0
if (flags == EMSdevice::EMS_DEVICE_FLAG_MM10) {
register_telegram_type(0x00AA, F("MMConfigMessage"), false, nullptr);
register_telegram_type(0x00AB, F("MMStatusMessage"), true, std::bind(&Mixing::process_MMStatusMessage, this, _1));
register_telegram_type(0x00AC, F("MMSetMessage"), false, nullptr);
}
Settings settings;
mqtt_format_ = settings.mqtt_format(); // single, nested or ha
// MQTT callbacks
// register_mqtt_topic("cmd", std::bind(&Mixing::cmd, this, _1));
@@ -75,44 +80,65 @@ void Mixing::show_values(uuid::console::Shell & shell) {
// ideally we should group up all the mixing units together into a nested JSON but for now we'll send them individually
void Mixing::publish_values() {
DynamicJsonDocument doc(EMSESP_MAX_JSON_SIZE_SMALL);
JsonObject rootMixing = doc.to<JsonObject>();
JsonObject dataMixing;
if (mqtt_format_ == Settings::MQTT_format::SINGLE) {
switch (type_) {
case Type::HC:
doc["type"] = F("hc");
rootMixing["type"] = F("hc");
break;
case Type::WWC:
doc["type"] = F("wwc");
rootMixing["type"] = F("wwc");
break;
case Type::NONE:
default:
return;
}
dataMixing = rootMixing;
} else {
char hc_name[10]; // hc{1-4}
if(type_ == Type::HC) {
strlcpy(hc_name, "hc", 10);
} else {
strlcpy(hc_name, "wwc", 10);
}
char s[3]; // for formatting strings
strlcat(hc_name, Helpers::itoa(s, hc_), 10);
dataMixing = rootMixing.createNestedObject(hc_name);
}
if (flowTemp_ != EMS_VALUE_USHORT_NOTSET) {
doc["flowTemp"] = (float)flowTemp_ / 10;
dataMixing["flowTemp"] = (float)flowTemp_ / 10;
}
if (pumpMod_ != EMS_VALUE_UINT_NOTSET) {
doc["pumpMod"] = pumpMod_;
dataMixing["pumpMod"] = pumpMod_;
}
if (status_ != EMS_VALUE_UINT_NOTSET) {
doc["status"] = status_;
dataMixing["status"] = status_;
}
if (flowSetTemp_ != EMS_VALUE_UINT_NOTSET) {
doc["flowSetTemp"] = flowSetTemp_;
dataMixing["flowSetTemp"] = flowSetTemp_;
}
#ifdef EMSESP_DEBUG
DEBUG_LOG(F("[DEBUG] Performing a mixing module publish"));
LOG_DEBUG(F("[DEBUG] Performing a mixing module publish"));
#endif
// if format is single, send immediately and quit
if (mqtt_format_ == Settings::MQTT_format::SINGLE) {
char topic[30];
char s[3]; // for formatting strings
strlcpy(topic, "mixing_data", 30);
strlcat(topic, Helpers::itoa(s, hc_), 30); // append hc to topic
Mqtt::publish(topic, doc);
return;
}
char topic[30];
strlcpy(topic, "mixing_data", 30);
Mqtt::publish(topic, doc);
}
// check to see if values have been updated

View File

@@ -63,6 +63,8 @@ class Mixing : public EMSdevice {
uint8_t status_ = EMS_VALUE_UINT_NOTSET;
uint8_t flowSetTemp_ = EMS_VALUE_UINT_NOTSET;
Type type_ = Type::NONE;
uint8_t mqtt_format_; // single, nested or ha
};
} // namespace emsesp

View File

@@ -27,6 +27,7 @@ MAKE_PSTR_WORD(ip)
MAKE_PSTR_WORD(nested)
MAKE_PSTR_WORD(single)
MAKE_PSTR_WORD(ha)
MAKE_PSTR_WORD(my)
MAKE_PSTR_WORD(publish_time)
MAKE_PSTR_WORD(publish)
MAKE_PSTR_WORD(connected)
@@ -93,11 +94,11 @@ void Mqtt::reconnect() {
#ifndef EMSESP_STANDALONE
mqttClient_.disconnect();
#endif
DEBUG_LOG(F("Reconnecting..."));
LOG_DEBUG(F("Reconnecting..."));
}
// MQTT setup
void Mqtt::start() {
void Mqtt::setup() {
// exit if already initialized
if (mqtt_start_) {
return;
@@ -142,10 +143,10 @@ void Mqtt::start() {
#endif
mqtt_connecting_ = false;
mqtt_last_connection_ = millis();
mqtt_last_connection_ = uuid::get_uptime();
mqtt_reconnect_delay_ = Mqtt::MQTT_RECONNECT_DELAY_MIN;
DEBUG_LOG(F("Configuring MQTT service..."));
LOG_DEBUG(F("Configuring MQTT service..."));
}
// MQTT init callbacks
@@ -161,28 +162,28 @@ void Mqtt::init() {
mqttClient_.onDisconnect([this](AsyncMqttClientDisconnectReason reason) {
if (reason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) {
logger_.err(F("Disconnected from server"));
LOG_DEBUG(F("Disconnected from server"));
}
if (reason == AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED) {
logger_.err(F("Server identifier Rejected"));
LOG_ERROR(F("Server identifier Rejected"));
}
if (reason == AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE) {
logger_.err(F("Server unavailable"));
LOG_ERROR(F("Server unavailable"));
}
if (reason == AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS) {
logger_.err(F("Malformed credentials"));
LOG_ERROR(F("Malformed credentials"));
}
if (reason == AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED) {
logger_.err(F("Not authorized"));
LOG_ERROR(F("Not authorized"));
}
// Reset reconnection delay
mqtt_last_connection_ = millis();
mqtt_last_connection_ = uuid::get_uptime();
mqtt_connecting_ = false;
mqtt_start_ = false; // will force a new start()
});
// mqttClient.onSubscribe([this](uint16_t packetId, uint8_t qos) { DEBUG_LOG(F("Subscribe ACK for PID %d"), packetId); });
// mqttClient.onSubscribe([this](uint16_t packetId, uint8_t qos) { LOG_DEBUG(F("Subscribe ACK for PID %d"), packetId); });
mqttClient_.onPublish([this](uint16_t packetId) { on_publish(packetId); });
@@ -236,7 +237,7 @@ void Mqtt::loop() {
}
// send out heartbeat
uint32_t currentMillis = millis();
uint32_t currentMillis = uuid::get_uptime();
if ((currentMillis - last_heartbeat_ > MQTT_HEARTBEAT_INTERVAL)) {
last_heartbeat_ = currentMillis;
send_heartbeat();
@@ -258,7 +259,7 @@ void Mqtt::loop() {
}
// We need to reconnect. Check when was the last time we tried this
if (mqtt_last_connection_ && (millis() - mqtt_last_connection_ < mqtt_reconnect_delay_)) {
if (mqtt_last_connection_ && (uuid::get_uptime() - mqtt_last_connection_ < mqtt_reconnect_delay_)) {
return;
}
@@ -271,8 +272,8 @@ void Mqtt::loop() {
}
#ifndef EMSESP_STANDALONE
start();
logger_.info(F("Connecting to MQTT server..."));
setup();
LOG_INFO(F("Connecting to the MQTT server..."));
mqttClient_.connect(); // Connect to the MQTT broker
#endif
}
@@ -338,7 +339,7 @@ void Mqtt::on_message(char * topic, char * payload, size_t len) {
strlcpy(message, payload, len + 1);
#ifdef EMSESP_DEBUG
DEBUG_LOG(F("Received %s => %s (length %d)"), topic, message, len);
LOG_DEBUG(F("Received %s => %s (length %d)"), topic, message, len);
#endif
// strip out everything until the last /
@@ -357,7 +358,7 @@ void Mqtt::on_message(char * topic, char * payload, size_t len) {
}
// if we got here we didn't find a topic match
DEBUG_LOG(F("No responding handler found for topic %s"), topic);
LOG_DEBUG(F("No responding handler found for topic %s"), topic);
}
// print all the topics related to a specific device_id
@@ -382,7 +383,7 @@ void Mqtt::show_topic_handlers(uuid::console::Shell & shell, const uint8_t devic
// and always remove from queue
void Mqtt::on_publish(uint16_t packetId) {
// find the MQTT message in the queue and remove it
if ((mqtt_messages_.empty()) || (mqtt_qos_ == 0)) {
if (mqtt_messages_.empty()) {
return;
}
@@ -394,9 +395,11 @@ void Mqtt::on_publish(uint16_t packetId) {
}
if (mqtt_message.packet_id_ == packetId) {
DEBUG_LOG(F("Acknowledged PID %d. Removing from queue"), packetId);
#ifdef EMSESP_DEBUG
LOG_DEBUG(F("Acknowledged PID %d. Removing from queue"), packetId);
#endif
} else {
DEBUG_LOG(F("Mismatch, expecting PID %d, got %d"), mqtt_message.packet_id_, packetId);
LOG_DEBUG(F("Mismatch, expecting PID %d, got %d"), mqtt_message.packet_id_, packetId);
mqtt_publish_fails_++; // increment error count
}
@@ -419,31 +422,26 @@ char * Mqtt::make_topic(char * result, const std::string & topic) {
return result;
}
void Mqtt::start() {
publish("status", "online", true); // say we're alive to the Last Will topic, with retain on
send_start_topic();
send_heartbeat(); // send heartbeat if enabled
}
// send online appended with the version information as JSON
void Mqtt::send_start_topic() {
StaticJsonDocument<90> doc;
doc["event"] = "start";
doc["version"] = Settings().app_version();
#ifndef EMSESP_STANDALONE
doc["IP"] = WiFi.localIP().toString();
#endif
publish("info", doc, false); // send with retain off
}
// MQTT onConnect - when a connect is established
void Mqtt::on_connect() {
DEBUG_LOG(F("MQTT connected"));
mqtt_reconnect_delay_ = Mqtt::MQTT_RECONNECT_DELAY_MIN;
mqtt_last_connection_ = millis();
mqtt_last_connection_ = uuid::get_uptime();
mqtt_connecting_ = false;
publish("status", "online", true); // say we're alive to the Last Will topic, with retain on
send_start_topic();
send_heartbeat(); // send heartbeat if enabled
LOG_INFO(F("MQTT connected"));
}
// send periodic MQTT message with system information
@@ -462,38 +460,6 @@ void Mqtt::send_heartbeat() {
publish("heartbeat", doc, false); // send to MQTT with retain off
}
// add MQTT message to queue, payload is a JSON doc.
// NOTE this only prints first 255 chars
void Mqtt::queue_publish_message(const std::string & topic, const JsonDocument & payload, const bool retain) {
// can't have bogus topics, but empty payloads are ok
if (topic.empty()) {
return;
}
/*
// check for empty JSON doc - we don't like those
size_t capacity = measureJson(payload);
if (capacity <= 3) {
// DEBUG_LOG(("Empty JSON payload for topic %s. Skipping"), topic);
return;
}
*/
std::string payload_text;
serializeJson(payload, payload_text);
auto message = std::make_shared<MqttMessage>(Operation::PUBLISH, topic, payload_text, retain);
// DEBUG_LOG(F("Adding JSON publish message created with topic %s, message %s"), topic, payload_text.c_str());
// if the queue is full, make room but removing the last one
if (mqtt_messages_.size() >= maximum_mqtt_messages_) {
mqtt_messages_.pop_front();
}
mqtt_messages_.emplace_back(mqtt_message_id_++, std::move(message));
}
// add MQTT message to queue, payload is a string
void Mqtt::queue_publish_message(const std::string & topic, const std::string & payload, const bool retain) {
// can't have bogus topics, but empty payloads are ok
@@ -518,7 +484,7 @@ void Mqtt::queue_subscribe_message(const std::string & topic) {
}
auto message = std::make_shared<MqttMessage>(Operation::SUBSCRIBE, topic, "", false);
DEBUG_LOG(F("Adding a subscription for %s"), topic.c_str());
LOG_DEBUG(F("Adding a subscription for %s"), topic.c_str());
// if the queue is full, make room but removing the last one
if (mqtt_messages_.size() >= maximum_mqtt_messages_) {
@@ -528,24 +494,27 @@ void Mqtt::queue_subscribe_message(const std::string & topic) {
mqtt_messages_.emplace_back(mqtt_message_id_++, std::move(message));
}
// Publish using the user's custom retain flag
void Mqtt::publish(const std::string & topic, const std::string & payload) {
publish(topic, payload, mqtt_retain_);
}
void Mqtt::publish(const std::string & topic, const JsonDocument & payload) {
publish(topic, payload, mqtt_retain_);
}
// MQTT Publish, using a specific retain flag
void Mqtt::publish(const std::string & topic, const std::string & payload, bool retain) {
queue_publish_message(topic, payload, retain);
}
// Publish using the user's custom retain flag
void Mqtt::publish(const std::string & topic, const std::string & payload) {
publish(topic, payload, mqtt_retain_);
}
void Mqtt::publish(const std::string & topic, const JsonDocument & payload) {
publish(topic, payload, mqtt_retain_);
}
void Mqtt::publish(const std::string & topic, const JsonDocument & payload, bool retain) {
queue_publish_message(topic, payload, retain);
// convert json to string
std::string payload_text;
serializeJson(payload, payload_text);
queue_publish_message(topic, payload_text, retain);
}
// for booleans, which get converted to string values 1 and 0
void Mqtt::publish(const std::string & topic, const bool value) {
queue_publish_message(topic, value ? "1" : "0", mqtt_retain_);
}
@@ -585,14 +554,14 @@ void Mqtt::process_queue() {
// if we're subscribing...
if (message->operation == Operation::SUBSCRIBE) {
DEBUG_LOG(F("Subscribing to topic: %s"), full_topic);
LOG_DEBUG(F("Subscribing to topic: %s"), full_topic);
#ifndef EMSESP_STANDALONE
uint16_t packet_id = mqttClient_.subscribe(full_topic, mqtt_qos_);
#else
uint16_t packet_id = 1;
#endif
if (!packet_id) {
DEBUG_LOG(F("Error subscribing to %s, error %d"), full_topic, packet_id);
LOG_DEBUG(F("Error subscribing to %s, error %d"), full_topic, packet_id);
}
mqtt_messages_.pop_front(); // remove the message from the queue
@@ -607,24 +576,25 @@ void Mqtt::process_queue() {
}
// else try and publish it
#ifndef EMSESP_STANDALONE
uint16_t packet_id = mqttClient_.publish(full_topic, mqtt_qos_, message->retain, message->payload.c_str());
// uint16_t packet_id = mqttClient_.publish(full_topic, mqtt_qos_, message->retain, message->payload.c_str());
uint16_t packet_id = mqttClient_.publish(full_topic, mqtt_qos_, message->retain, message->payload.c_str(), message->payload.size(), false, mqtt_message.id_);
#else
uint16_t packet_id = 1;
#endif
DEBUG_LOG(F("Publishing topic %s (#%02d, attempt #%d, pid %d)"), full_topic, mqtt_message.id_, mqtt_message.retry_count_ + 1, packet_id);
LOG_DEBUG(F("Publishing topic %s (#%02d, attempt #%d, pid %d)"), full_topic, mqtt_message.id_, mqtt_message.retry_count_ + 1, packet_id);
if (packet_id == 0) {
// it failed. if we retried n times, give up. remove from queue
if (mqtt_message.retry_count_ == (MQTT_PUBLISH_MAX_RETRY - 1)) {
logger_.err(F("Failed to publish to %s after %d attempts"), full_topic, mqtt_message.retry_count_ + 1);
LOG_ERROR(F("Failed to publish to %s after %d attempts"), full_topic, mqtt_message.retry_count_ + 1);
mqtt_publish_fails_++; // increment failure counter
mqtt_messages_.pop_front(); // delete
return;
} else {
mqtt_messages_.front().retry_count_++;
// logger_.err(F("Failed to publish to %s. Trying again, #%d"), full_topic, mqtt_message.retry_count_ + 1);
DEBUG_LOG(F("Failed to publish to %s. Trying again, #%d"), full_topic, mqtt_message.retry_count_ + 1);
LOG_DEBUG(F("Failed to publish to %s. Trying again, #%d"), full_topic, mqtt_message.retry_count_ + 1);
return; // leave on queue for next time so it gets republished
}
}
@@ -633,7 +603,9 @@ void Mqtt::process_queue() {
// but add the packet_id so we can check it later
if (mqtt_qos_ != 0) {
mqtt_messages_.front().packet_id_ = packet_id;
DEBUG_LOG(F("Setting packetID for ACK to %d"), packet_id);
#ifdef EMSESP_DEBUG
LOG_DEBUG(F("Setting packetID for ACK to %d"), packet_id);
#endif
return;
}
@@ -678,6 +650,8 @@ void Mqtt::console_commands(Shell & shell, unsigned int context) {
value = Settings::MQTT_format::NESTED;
} else if (arguments[0] == read_flash_string(F_(ha))) {
value = Settings::MQTT_format::HA;
} else if (arguments[0] == read_flash_string(F_(my))) {
value = Settings::MQTT_format::MY;
} else {
shell.println(F("Must be single, nested or ha"));
return;
@@ -687,7 +661,7 @@ void Mqtt::console_commands(Shell & shell, unsigned int context) {
shell.println(F("Please restart EMS-ESP"));
},
[](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments __attribute__((unused))) -> const std::vector<std::string> {
return std::vector<std::string>{read_flash_string(F_(single)), read_flash_string(F_(nested)), read_flash_string(F_(ha))};
return std::vector<std::string>{read_flash_string(F_(single)), read_flash_string(F_(nested)), read_flash_string(F_(ha)), read_flash_string(F_(my))};
});
EMSESPShell::commands->add_command(ShellContext::MQTT,
@@ -852,6 +826,8 @@ void Mqtt::console_commands(Shell & shell, unsigned int context) {
shell.printfln(F_(mqtt_format_fmt), F_(nested));
} else if (settings.mqtt_format() == Settings::MQTT_format::HA) {
shell.printfln(F_(mqtt_format_fmt), F_(ha));
} else if (settings.mqtt_format() == Settings::MQTT_format::MY) {
shell.printfln(F_(mqtt_format_fmt), F_(my));
}
shell.printfln(F_(mqtt_heartbeat_fmt), settings.mqtt_heartbeat() ? F_(enabled) : F_(disabled));
shell.printfln(F_(mqtt_publish_time_fmt), settings.mqtt_publish_time());

View File

@@ -63,6 +63,7 @@ struct MqttMessage {
class Mqtt {
public:
void loop();
void start();
void send_heartbeat();
enum Operation { PUBLISH, SUBSCRIBE };
@@ -109,32 +110,28 @@ class Mqtt {
};
static std::deque<QueuedMqttMessage> mqtt_messages_;
void start();
#ifndef EMSESP_STANDALONE
static AsyncMqttClient mqttClient_;
#endif
void flush_message_queue();
void setup();
static constexpr size_t MAX_MQTT_MESSAGES = 50;
static size_t maximum_mqtt_messages_;
static uint16_t mqtt_message_id_;
static bool mqtt_retain_;
static constexpr uint8_t MQTT_QUEUE_MAX_SIZE = 50;
static constexpr uint32_t MQTT_PUBLISH_WAIT = 750; // delay between sending publishes, although it should be asynchronous!
static constexpr uint32_t MQTT_PUBLISH_WAIT = 750; // delay between sending publishes, to account for large payloads
static constexpr uint8_t MQTT_PUBLISH_MAX_RETRY = 3; // max retries for giving up on publishing
static constexpr uint8_t MQTT_KEEP_ALIVE = 60;
static constexpr uint8_t MQTT_KEEP_ALIVE = 60; // 60 seconds. This could also be less, like 30 seconds
static constexpr uint32_t MQTT_RECONNECT_DELAY_MIN = 2000; // Try to reconnect in 2 seconds upon disconnection
static constexpr uint32_t MQTT_RECONNECT_DELAY_STEP = 3000; // Increase the reconnect delay in 3 seconds after each failed attempt
static constexpr uint32_t MQTT_RECONNECT_DELAY_MAX = 120000; // Set reconnect time to 2 minutes at most
static constexpr uint32_t MQTT_HEARTBEAT_INTERVAL = 60000; // in milliseconds, how often the MQTT heartbeat is sent (1 min)
static bool mqtt_retain_;
static void queue_publish_message(const std::string & topic, const JsonDocument & payload, const bool retain);
static void queue_publish_message(const std::string & topic, const std::string & payload, const bool retain);
static void queue_subscribe_message(const std::string & topic);
void on_publish(uint16_t packetId);

View File

@@ -36,6 +36,7 @@ void Network::start() {
WiFi.persistent(false);
WiFi.disconnect(true);
WiFi.setAutoReconnect(false);
WiFi.mode(WIFI_STA);
#endif
#if defined(ESP8266)
@@ -71,7 +72,7 @@ void Network::sta_mode_start(WiFiEvent_t event, WiFiEventInfo_t info) {
#if defined(ESP8266)
void Network::sta_mode_connected(const WiFiEventStationModeConnected & event) {
logger_.info(F("Connected to %s (%02X:%02X:%02X:%02X:%02X:%02X) on channel %u"),
LOG_INFO(F("Connected to %s (%02X:%02X:%02X:%02X:%02X:%02X) on channel %u"),
event.ssid.c_str(),
event.bssid[0],
event.bssid[1],
@@ -86,7 +87,7 @@ void Network::sta_mode_connected(const WiFiEventStationModeConnected & event) {
}
#elif defined(ESP32)
void Network::sta_mode_connected(WiFiEvent_t event, WiFiEventInfo_t info) {
logger_.info(F("Connected to %s (%02X:%02X:%02X:%02X:%02X:%02X) on channel %u"),
LOG_INFO(F("Connected to %s (%02X:%02X:%02X:%02X:%02X:%02X) on channel %u"),
info.connected.ssid,
info.sta_connected.mac[0],
info.sta_connected.mac[1],
@@ -107,10 +108,10 @@ void Network::sta_mode_disconnected(const WiFiEventStationModeDisconnected & eve
if (event.reason == 201) {
if (++disconnect_count_ == 3) {
if (System::safe_mode()) {
logger_.err(F("Failed to connect to WiFi %s after %d attempts"), event.ssid.c_str(), disconnect_count_ - 1);
LOG_ERROR(F("Failed to connect to WiFi %s after %d attempts"), event.ssid.c_str(), disconnect_count_ - 1);
disconnect_count_ = 0;
} else {
logger_.err(F("Failed to connect to WiFi. Rebooting into Safe mode"));
LOG_ERROR(F("Failed to connect to WiFi. Rebooting into Safe mode"));
System::restart(true); // set safe mode and restart
}
}
@@ -118,20 +119,20 @@ void Network::sta_mode_disconnected(const WiFiEventStationModeDisconnected & eve
}
#elif defined(ESP32)
void Network::sta_mode_disconnected(WiFiEvent_t event, WiFiEventInfo_t info) {
// logger_.err(F("Failed to connect to WiFi %s, reason code %d"), info.disconnected.ssid, info.disconnected.reason);
// LOG_ERROR(F("Failed to connect to WiFi %s, reason code %d"), info.disconnected.ssid, info.disconnected.reason);
}
#endif
#if defined(ESP8266)
void Network::sta_mode_got_ip(const WiFiEventStationModeGotIP & event) {
logger_.info(F("Obtained IPv4 address %s/%s and gateway %s"),
LOG_INFO(F("Obtained IPv4 address %s/%s and gateway %s"),
uuid::printable_to_string(event.ip).c_str(),
uuid::printable_to_string(event.mask).c_str(),
uuid::printable_to_string(event.gw).c_str());
}
#elif defined(ESP32)
void Network::sta_mode_got_ip(WiFiEvent_t event, WiFiEventInfo_t info) {
logger_.info(F("Obtained IPv4 address %s/%s and gateway %s"),
LOG_INFO(F("Obtained IPv4 address %s/%s and gateway %s"),
uuid::printable_to_string(IPAddress(info.got_ip.ip_info.ip.addr)).c_str(),
uuid::printable_to_string(IPAddress(info.got_ip.ip_info.netmask.addr)).c_str(),
uuid::printable_to_string(IPAddress(info.got_ip.ip_info.gw.addr)).c_str());
@@ -202,21 +203,21 @@ void Network::ota_setup() {
ota_->setPassword(settings.admin_password().c_str());
ota_->onStart([this]() {
DEBUG_LOG(F("OTA starting (send type %d)..."), ota_->getCommand());
LOG_DEBUG(F("OTA starting (send type %d)..."), ota_->getCommand());
// turn off stuff to stop interference
EMSuart::stop(); // UART stop
in_ota_ = true; // set flag so all other services stop
});
ota_->onEnd([this]() { DEBUG_LOG(F("OTA done, automatically restarting")); });
ota_->onEnd([this]() { LOG_DEBUG(F("OTA done, automatically restarting")); });
ota_->onProgress([this](unsigned int progress, unsigned int total) {
/*
static unsigned int _progOld;
unsigned int _prog = (progress / (total / 100));
if (_prog != _progOld) {
DEBUG_LOG(F("[OTA] Progress: %u%%"), _prog);
LOG_DEBUG(F("[OTA] Progress: %u%%"), _prog);
_progOld = _prog;
}
*/
@@ -224,23 +225,23 @@ void Network::ota_setup() {
ota_->onError([this](ota_error_t error) {
if (error == OTA_AUTH_ERROR) {
logger_.err(F("[OTA] Auth Failed"));
LOG_ERROR(F("[OTA] Auth Failed"));
} else if (error == OTA_BEGIN_ERROR) {
logger_.err(F("[OTA] Begin Failed"));
LOG_ERROR(F("[OTA] Begin Failed"));
} else if (error == OTA_CONNECT_ERROR) {
logger_.err(F("[OTA] Connect Failed"));
LOG_ERROR(F("[OTA] Connect Failed"));
} else if (error == OTA_RECEIVE_ERROR) {
logger_.err(F("[OTA] Receive Failed"));
LOG_ERROR(F("[OTA] Receive Failed"));
} else if (error == OTA_END_ERROR) {
logger_.err(F("[OTA] End Failed"));
LOG_ERROR(F("[OTA] End Failed"));
} else {
logger_.err(F("[OTA] Error %d"), error);
LOG_ERROR(F("[OTA] Error %d"), error);
};
});
// start ota service
ota_->begin();
logger_.info(F("Listening to firmware updates on %s.local:%u"), ota_->getHostname().c_str(), OTA_PORT);
LOG_INFO(F("Listening to firmware updates on %s.local:%u"), ota_->getHostname().c_str(), OTA_PORT);
#endif
}

View File

@@ -30,6 +30,23 @@ void Sensors::start() {
// copy over values from MQTT so we don't keep on quering the filesystem
mqtt_format_ = Settings().mqtt_format();
// if we're using HA MQTT Discovery, send out the config
// currently we just do this for a single sensor (sensor1)
if (mqtt_format_ == Settings::MQTT_format::HA) {
// Mqtt::publish(topic); // empty payload, this remove any previous config sent to HA
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_MEDIUM> doc;
doc["dev_cla"] = "temperature";
doc["name"] = "ems-esp-sensor1";
doc["uniq_id"] = "ems-esp-sensor1";
doc["~"] = "homeassistant/sensor/ems-esp/external";
doc["stat_t"] = "~/state";
doc["unit_of_meas"] = "°C";
doc["val_tpl"] = "{{value_json.sensor1.temp}}";
Mqtt::publish("homeassistant/sensor/ems-esp/external/config", doc, true); // publish the config payload with retain flag
}
#ifndef EMSESP_STANDALONE
bus_.begin(SENSOR_GPIO);
#endif
@@ -37,9 +54,11 @@ void Sensors::start() {
void Sensors::loop() {
#ifndef EMSESP_STANDALONE
uint32_t time_now = uuid::get_uptime();
if (state_ == State::IDLE) {
if (millis() - last_activity_ >= READ_INTERVAL_MS) {
// DEBUG_LOG(F("Read sensor temperature")); // uncomment for debug
if (time_now - last_activity_ >= READ_INTERVAL_MS) {
// LOG_DEBUG(F("Read sensor temperature")); // uncomment for debug
if (bus_.reset()) {
bus_.skip();
bus_.write(CMD_CONVERT_TEMP);
@@ -47,30 +66,30 @@ void Sensors::loop() {
state_ = State::READING;
} else {
// no sensors found
// logger_.err(F("Bus reset failed")); // uncomment for debug
// LOG_ERROR(F("Bus reset failed")); // uncomment for debug
devices_.clear(); // remove all know devices incase we have a disconnect
}
last_activity_ = millis();
last_activity_ = time_now;
}
} else if (state_ == State::READING) {
if (temperature_convert_complete()) {
// DEBUG_LOG(F("Scanning for sensors")); // uncomment for debug
// LOG_DEBUG(F("Scanning for sensors")); // uncomment for debug
bus_.reset_search();
found_.clear();
state_ = State::SCANNING;
last_activity_ = millis();
} else if (millis() - last_activity_ > READ_TIMEOUT_MS) {
logger_.err(F("Sensor read timeout"));
last_activity_ = time_now;
} else if (time_now - last_activity_ > READ_TIMEOUT_MS) {
LOG_ERROR(F("Sensor read timeout"));
state_ = State::IDLE;
last_activity_ = millis();
last_activity_ = time_now;
}
} else if (state_ == State::SCANNING) {
if (millis() - last_activity_ > SCAN_TIMEOUT_MS) {
logger_.err(F("Sensor scan timeout"));
if (time_now - last_activity_ > SCAN_TIMEOUT_MS) {
LOG_ERROR(F("Sensor scan timeout"));
state_ = State::IDLE;
last_activity_ = millis();
last_activity_ = time_now;
} else {
uint8_t addr[ADDR_LEN] = {0};
@@ -89,26 +108,26 @@ void Sensors::loop() {
/*
// comment out for debugging
char result[10];
DEBUG_LOG(F("Temp of %s = %s"),
LOG_DEBUG(F("Temp of %s = %s"),
found_.back().to_string().c_str(),
Helpers::render_value(result, found_.back().temperature_c_, 2));
*/
break;
default:
logger_.err(F("Unknown sensor %s"), Device(addr).to_string().c_str());
LOG_ERROR(F("Unknown sensor %s"), Device(addr).to_string().c_str());
break;
}
} else {
logger_.err(F("Invalid sensor %s"), Device(addr).to_string().c_str());
LOG_ERROR(F("Invalid sensor %s"), Device(addr).to_string().c_str());
}
} else {
bus_.depower();
devices_ = std::move(found_);
found_.clear();
// DEBUG_LOG(F("Found %zu sensor(s). Adding them."), devices_.size()); // uncomment for debug
// LOG_DEBUG(F("Found %zu sensor(s). Adding them."), devices_.size()); // uncomment for debug
state_ = State::IDLE;
last_activity_ = millis();
last_activity_ = time_now;
}
}
}
@@ -129,7 +148,7 @@ bool Sensors::temperature_convert_complete() {
float Sensors::get_temperature_c(const uint8_t addr[]) {
#ifndef EMSESP_STANDALONE
if (!bus_.reset()) {
logger_.err(F("Bus reset failed before reading scratchpad from %s"), Device(addr).to_string().c_str());
LOG_ERROR(F("Bus reset failed before reading scratchpad from %s"), Device(addr).to_string().c_str());
return NAN;
}
@@ -140,12 +159,12 @@ float Sensors::get_temperature_c(const uint8_t addr[]) {
bus_.read_bytes(scratchpad, SCRATCHPAD_LEN);
if (!bus_.reset()) {
logger_.err(F("Bus reset failed after reading scratchpad from %s"), Device(addr).to_string().c_str());
LOG_ERROR(F("Bus reset failed after reading scratchpad from %s"), Device(addr).to_string().c_str());
return NAN;
}
if (bus_.crc8(scratchpad, SCRATCHPAD_LEN - 1) != scratchpad[SCRATCHPAD_LEN - 1]) {
logger_.warning(F("Invalid scratchpad CRC: %02X%02X%02X%02X%02X%02X%02X%02X%02X from device %s"),
LOG_WARNING(F("Invalid scratchpad CRC: %02X%02X%02X%02X%02X%02X%02X%02X%02X from device %s"),
scratchpad[0],
scratchpad[1],
scratchpad[2],
@@ -203,7 +222,6 @@ uint64_t Sensors::Device::id() const {
std::string Sensors::Device::to_string() const {
std::string str(20, '\0');
snprintf_P(&str[0],
str.capacity() + 1,
PSTR("%02X-%04X-%04X-%04X-%02X"),
@@ -214,6 +232,18 @@ std::string Sensors::Device::to_string() const {
(unsigned int)(id_)&0xFF);
return str;
}
std::string Sensors::Device::to_stringc() const {
std::string str(20, '\0');
snprintf_P(&str[0],
str.capacity() + 1,
PSTR("%02X%04X%04X%04X%02X"),
(unsigned int)(id_ >> 56) & 0xFF,
(unsigned int)(id_ >> 40) & 0xFFFF,
(unsigned int)(id_ >> 24) & 0xFFFF,
(unsigned int)(id_ >> 8) & 0xFFFF,
(unsigned int)(id_)&0xFF);
return str;
}
// send all dallas sensor values as a JSON package to MQTT
// assumes there are devices
@@ -227,7 +257,7 @@ void Sensors::publish_values() {
// if we're not using nested JSON, send each sensor out seperately
// sensor1, sensor2 etc...
// e.g. sensor_1 = {"temp":20.2}
if (mqtt_format_ != Settings::MQTT_format::NESTED) {
if (mqtt_format_ == Settings::MQTT_format::SINGLE) {
StaticJsonDocument<100> doc;
for (const auto & device : devices_) {
char s[5];
@@ -242,18 +272,23 @@ void Sensors::publish_values() {
}
// group all sensors together - https://github.com/proddy/EMS-ESP/issues/327
// https://arduinojson.org/v6/assistant/
// This is used for both NESTED and HA modes
// sensors = {
// "sensor1":{"id":"28-EA41-9497-0E03-5F","temp":"23.25"},
// "sensor2":{"id":"28-EA41-9497-0E03-5F","temp":"23.25"},
// "sensor3":{"id":"28-EA41-9497-0E03-5F","temp":"23.25"},
// "sensor4":{"id":"28-EA41-9497-0E03-5F","temp":"23.25"}
// }
// const size_t capacity = num_devices * JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(num_devices);
DynamicJsonDocument doc(100 * num_devices);
uint8_t i = 1;
for (const auto & device : devices_) {
if (mqtt_format_ == Settings::MQTT_format::MY) {
char s[5];
doc[device.to_stringc()] = Helpers::render_value(s, device.temperature_c_, 2);
} else {
char sensorID[10]; // sensor{1-n}
strlcpy(sensorID, "sensor", 10);
char s[5];
@@ -262,8 +297,13 @@ void Sensors::publish_values() {
dataSensor["id"] = device.to_string();
dataSensor["temp"] = Helpers::render_value(s, device.temperature_c_, 2);
}
}
if (mqtt_format_ == Settings::MQTT_format::HA) {
Mqtt::publish("homeassistant/sensor/ems-esp/external/state", doc);
} else {
Mqtt::publish("sensors", doc);
}
}
} // namespace emsesp

View File

@@ -46,6 +46,7 @@ class Sensors {
uint64_t id() const;
std::string to_string() const;
std::string to_stringc() const;
float temperature_c_ = NAN;
@@ -66,8 +67,7 @@ class Sensors {
#if defined(ESP8266)
static constexpr uint8_t SENSOR_GPIO = 14; // D5
#elif defined(ESP32)
// static constexpr uint8_t SENSOR_GPIO = 14; // same position
static constexpr uint8_t SENSOR_GPIO = 18; // for Wemos D1 32
static constexpr uint8_t SENSOR_GPIO = 18; // Wemos D1-32 for compatibility D5
#endif
enum class State { IDLE, READING, SCANNING };
@@ -101,8 +101,8 @@ class Sensors {
bool temperature_convert_complete();
float get_temperature_c(const uint8_t addr[]);
uint32_t last_activity_ = millis();
uint32_t last_publish_ = millis();
uint32_t last_activity_ = uuid::get_uptime();
uint32_t last_publish_ = uuid::get_uptime();
State state_ = State::IDLE;
std::vector<Device> found_;
std::vector<Device> devices_;

View File

@@ -139,10 +139,10 @@ Settings::Settings() {
if (EMSESP_FS.begin(true)) {
#endif
#endif
logger_.info(F("Mounted filesystem"));
LOG_INFO(F("Mounted filesystem"));
mounted_ = true;
} else {
logger_.alert(F("Unable to mount filesystem"));
LOG_ERROR(F("Unable to mount filesystem"));
unavailable_ = true;
}
}
@@ -154,7 +154,7 @@ Settings::Settings() {
}
if (!loaded_) {
logger_.err(F("Failed to load settings. Using defaults"));
LOG_ERROR(F("Failed to load settings. Using defaults"));
read_settings(ArduinoJson::StaticJsonDocument<0>());
loaded_ = true;
}
@@ -169,7 +169,7 @@ void Settings::commit() {
EMSuart::stop(); // temporary suspend UART because is can cause interference on the UART
logger_.debug(F("Saving settings"));
LOG_DEBUG(F("Saving settings"));
if (write_settings(filename)) {
if (read_settings(filename, false)) {
write_settings(backup_filename);
@@ -190,21 +190,21 @@ bool Settings::read_settings(const std::string & filename, bool load) {
auto error = ArduinoJson::deserializeMsgPack(doc, file);
if (error) {
logger_.err(F("Failed to parse settings file %s: %s"), filename.c_str(), error.c_str());
LOG_ERROR(F("Failed to parse settings file %s: %s"), filename.c_str(), error.c_str());
return false;
} else {
if (load) {
logger_.info(F("Loading settings from file %s"), filename.c_str());
LOG_INFO(F("Loading settings from file %s"), filename.c_str());
read_settings(doc);
}
return true;
}
} else {
logger_.err(F("Settings file %s does not exist"), filename.c_str());
LOG_ERROR(F("Settings file %s does not exist"), filename.c_str());
return false;
}
#else
logger_.info(F("Loading settings from file %s (%d)"), filename.c_str(), load);
LOG_INFO(F("Loading settings from file %s (%d)"), filename.c_str(), load);
return false;
#endif
}
@@ -221,17 +221,17 @@ bool Settings::write_settings(const std::string & filename) {
ArduinoJson::serializeMsgPack(doc, file);
if (file.getWriteError()) {
logger_.alert(F("Failed to write settings file %s: %u"), filename.c_str(), file.getWriteError());
LOG_ERROR(F("Failed to write settings file %s: %u"), filename.c_str(), file.getWriteError());
return false;
} else {
return true;
}
} else {
logger_.alert(F("Unable to open settings file %s for writing"), filename.c_str());
LOG_ERROR(F("Unable to open settings file %s for writing"), filename.c_str());
return false;
}
#else
logger_.debug(F("Write settings file %s"), filename.c_str());
LOG_DEBUG(F("Write settings file %s"), filename.c_str());
return false;
#endif
}

View File

@@ -51,13 +51,13 @@
#define EMSESP_DEFAULT_MQTT_PORT 1884
#define EMSESP_DEFAULT_MQTT_QOS 0
#define EMSESP_DEFAULT_MQTT_RETAIN false
#define EMSESP_DEFAULT_MQTT_FORMAT 2 // nested
#define EMSESP_DEFAULT_MQTT_FORMAT 2 // 2=nested
#define EMSESP_DEFAULT_MQTT_HEARTBEAT true
#define EMSESP_DEFAULT_EMS_READ_ONLY false
#define EMSESP_DEFAULT_SHOWER_TIMER false
#define EMSESP_DEFAULT_SHOWER_ALERT false
#define EMSESP_DEFAULT_SYSLOG_INTERVAL 0
#define EMSESP_DEFAULT_MASTER_THERMOSTAT 0 // not set
#define EMSESP_DEFAULT_MASTER_THERMOSTAT 0 // 0=not set
#ifndef EMSESP_STANDALONE
#define EMSESP_DEFAULT_MQTT_PUBLISH_TIME 10
@@ -148,7 +148,7 @@ class Settings {
uint8_t master_thermostat() const;
void master_thermostat(const uint8_t & master_thermostat);
enum MQTT_format : uint8_t { SINGLE = 1, NESTED, HA };
enum MQTT_format : uint8_t { SINGLE = 1, NESTED, HA, MY };
uint8_t mqtt_format() const;
void mqtt_format(const uint8_t & mqtt_format);

View File

@@ -36,7 +36,8 @@ void Shower::loop() {
return;
}
uint32_t time_now = millis();
uint32_t time_now = uuid::get_uptime();
// if already in cold mode, ignore all this logic until we're out of the cold blast
if (!doing_cold_shot_) {
// is the hot water running?
@@ -55,7 +56,7 @@ void Shower::loop() {
if (!shower_on_ && (time_now - timer_start_) > SHOWER_MIN_DURATION) {
shower_on_ = true;
Mqtt::publish("shower_active", (bool)true);
DEBUG_LOG(F("[Shower] hot water still running, starting shower timer"));
LOG_DEBUG(F("[Shower] hot water still running, starting shower timer"));
}
// check if the shower has been on too long
else if ((((time_now - timer_start_) > SHOWER_MAX_DURATION) && !doing_cold_shot_) && shower_alert_) {
@@ -76,7 +77,7 @@ void Shower::loop() {
duration_ = (timer_pause_ - timer_start_ - SHOWER_OFFSET_TIME);
if (duration_ > SHOWER_MIN_DURATION) {
Mqtt::publish("shower_active", (bool)false);
DEBUG_LOG(F("[Shower] finished with duration %d"), duration_);
LOG_DEBUG(F("[Shower] finished with duration %d"), duration_);
publish_values();
}
}
@@ -94,7 +95,7 @@ void Shower::loop() {
// turn back on the hot water for the shower
void Shower::shower_alert_stop() {
if (doing_cold_shot_) {
DEBUG_LOG(F("Shower Alert stopped"));
LOG_DEBUG(F("Shower Alert stopped"));
// Boiler::set_tapwarmwater_activated(true);
doing_cold_shot_ = false;
// showerColdShotStopTimer.detach(); // disable the timer
@@ -104,7 +105,7 @@ void Shower::shower_alert_stop() {
// turn off hot water to send a shot of cold
void Shower::shower_alert_start() {
if (shower_alert_) {
DEBUG_LOG(F("Shower Alert started!"));
LOG_DEBUG(F("Shower Alert started!"));
// Boiler::set_tapwarmwater_activated(false);
doing_cold_shot_ = true;
// start the timer for n seconds which will reset the water back to hot

View File

@@ -29,7 +29,7 @@ uuid::log::Logger Solar::logger_{F_(logger_name), uuid::log::Facility::CONSOLE};
Solar::Solar(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
DEBUG_LOG(F("Registering new Solar module with device ID 0x%02X"), device_id);
LOG_DEBUG(F("Registering new Solar module with device ID 0x%02X"), device_id);
// telegram handlers
register_telegram_type(0x0097, F("SM10Monitor"), true, std::bind(&Solar::process_SM10Monitor, this, _1));
@@ -108,7 +108,7 @@ void Solar::publish_values() {
}
#ifdef EMSESP_DEBUG
DEBUG_LOG(F("[DEBUG] Performing a solar module publish"));
LOG_DEBUG(F("[DEBUG] Performing a solar module publish"));
#endif
Mqtt::publish("sm_data", doc);

View File

@@ -60,7 +60,7 @@ void System::mqtt_commands(const char * message) {
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
DeserializationError error = deserializeJson(doc, message);
if (error) {
DEBUG_LOG(F("MQTT error: payload %s, error %s"), message, error.c_str());
LOG_DEBUG(F("MQTT error: payload %s, error %s"), message, error.c_str());
return;
}
const char * command = doc["cmd"];
@@ -91,9 +91,9 @@ void System::restart(bool mode) {
// check for safe mode
if (mode) {
logger_.notice("Restarting system in safe mode...");
LOG_NOTICE("Restarting system in safe mode...");
} else {
logger_.notice("Restarting system...");
LOG_NOTICE("Restarting system...");
}
Shell::loop_all();
@@ -140,9 +140,11 @@ void System::start() {
settings.app_version(EMSESP_APP_VERSION);
settings.commit();
logger_.info(F("System booted (EMS-ESP version %s)"), settings.app_version().c_str());
LOG_INFO(F("System booted (EMS-ESP version %s)"), settings.app_version().c_str());
pinMode(LED_GPIO, OUTPUT); // LED pin
if (LED_GPIO) {
pinMode(LED_GPIO, OUTPUT); // LED pin, 0 is disabled
}
// register MQTT system commands
Mqtt::subscribe("cmd", std::bind(&System::mqtt_commands, this, _1));
@@ -218,19 +220,11 @@ void System::loop() {
syslog_.loop();
#endif
if (LED_GPIO) {
led_monitor(); // check status and report back using the LED
}
system_check(); // check system health
/*
#ifdef EMSESP_DEBUG
static uint32_t last_debug_ = 0;
if (millis() - last_debug_ >= 5000) {
last_debug_ = millis();
show_mem("loop");
}
#endif
*/
}
void System::show_mem(const char * text) {
@@ -239,7 +233,7 @@ void System::show_mem(const char * text) {
#else
uint32_t mem = 1000;
#endif
logger_.notice(F("{%s} Free mem: %ld (%d%%)"), text, mem, (100 * mem / heap_start_));
LOG_NOTICE(F("{%s} Free mem: %ld (%d%%)"), text, mem, (100 * mem / heap_start_));
}
// sets rate of led flash
@@ -252,9 +246,8 @@ void System::set_led_speed(uint32_t speed) {
void System::system_check() {
static uint32_t last_system_check_ = 0;
uint32_t currentMillis = millis();
if (!last_system_check_ || ((uint32_t)(currentMillis - last_system_check_) >= SYSTEM_CHECK_FREQUENCY)) {
last_system_check_ = currentMillis;
if (!last_system_check_ || ((uint32_t)(uuid::get_uptime() - last_system_check_) >= SYSTEM_CHECK_FREQUENCY)) {
last_system_check_ = uuid::get_uptime();
#ifndef EMSESP_STANDALONE
if ((WiFi.status() != WL_CONNECTED) || safe_mode()) {
@@ -268,23 +261,25 @@ void System::system_check() {
if (!EMSbus::bus_connected()) {
system_healthy_ = false;
set_led_speed(LED_WARNING_BLINK); // flash every 1/2 second from now on
LOG_ERROR(F("No connection to the EMS bus!"));
} else {
// if it was unhealthy but now we're better, make sure the LED is solid again cos we've been healed
if (!system_healthy_) {
system_healthy_ = true;
if (LED_GPIO) {
digitalWrite(LED_GPIO, LED_ON); // LED on, for ever
}
}
}
}
}
// flashes the LED
void System::led_monitor() {
static uint32_t led_last_blink_ = 0;
uint32_t currentMillis = millis();
if (!led_last_blink_ || (uint32_t)(currentMillis - led_last_blink_) >= led_flash_speed_) {
led_last_blink_ = currentMillis;
if (!led_last_blink_ || (uint32_t)(uuid::get_uptime() - led_last_blink_) >= led_flash_speed_) {
led_last_blink_ = uuid::get_uptime();
// if bus_not_connected or network not connected, start flashing
if (!system_healthy_) {

View File

@@ -60,7 +60,7 @@ class System {
static void restart(bool safe_mode);
static void restart() {
restart(false); // no safe mode
restart(false); // default, don't boot into safe mode
}
static void show_mem(const char * text);
@@ -74,8 +74,8 @@ class System {
static uuid::syslog::SyslogService syslog_;
#endif
static constexpr uint32_t SYSTEM_CHECK_FREQUENCY = 5000; // check every 5 seconds
static constexpr uint32_t LED_WARNING_BLINK = 1000; // pulse to show no connection
static constexpr uint32_t SYSTEM_CHECK_FREQUENCY = 10000; // check every 10 seconds
static constexpr uint32_t LED_WARNING_BLINK = 1000; // pulse to show no connection, 1 sec
static constexpr uint32_t LED_WARNING_BLINK_FAST = 100; // flash quickly for boot up sequence or safe-mode
// internal LED
@@ -108,6 +108,7 @@ class System {
static int reset_counter_;
static EMSuart emsuart_;
#if defined(ESP8266)
static RTCVars state_;
#endif

View File

@@ -200,9 +200,10 @@ void RxService::flush_rx_queue() {
rx_telegram_id_ = 0;
}
// start and initialize the Rx incoming buffer. Not currently used.
// start and initialize the Rx incoming buffer
void RxService::start() {
// DEBUG_LOG(F("RxStart"));
// LOG_DEBUG(F("RxStart"));
// function not currently used
}
// Rx loop, run as many times as you can
@@ -210,11 +211,10 @@ void RxService::start() {
void RxService::loop() {
#ifndef EMSESP_STANDALONE
// give rx some breathing space
uint32_t time_now = millis();
if ((time_now - last_rx_check_) < RX_LOOP_WAIT) {
if ((uuid::get_uptime() - last_rx_check_) < RX_LOOP_WAIT) {
return;
}
last_rx_check_ = time_now;
last_rx_check_ = uuid::get_uptime();
#endif
while (!rx_telegrams_.empty()) {
@@ -235,12 +235,13 @@ void RxService::loop() {
// add a new rx telegram object
// data is the whole telegram, assuming last byte holds the CRC
// length includes the CRC
// for EMS+ the type_id has the value + 256. We look for these type of telegrams with F7, F9 and FF in 3rd byte
void RxService::add(uint8_t * data, uint8_t length) {
// validate the CRC
uint8_t crc = calculate_crc(data, length - 1);
if (data[length - 1] != crc) {
TRACE_LOG(F("Rx: %s %s(BAD, CRC %02X != %02X)%s"), Helpers::data_to_hex(data, length).c_str(), COLOR_RED, data[length - 1], crc, COLOR_RESET);
LOG_TRACE(F("Rx: %s %s(BAD, CRC %02X != %02X)%s"), Helpers::data_to_hex(data, length).c_str(), COLOR_RED, data[length - 1], crc, COLOR_RESET);
increment_telegram_error_count();
return;
}
@@ -253,9 +254,9 @@ void RxService::add(uint8_t * data, uint8_t length) {
ems_mask(data[0]);
}
// if we're in read only mode, just dump out to console
if (EMSESP::ems_read_only()) {
TRACE_LOG(F("Rx: %s"), Helpers::data_to_hex(data, length).c_str());
// if we're in "trace" and "raw" print out actual telegram
if (logger_.enabled(Level::TRACE) && EMSESP::trace_raw()) {
LOG_TRACE(F("Rx: %s"), Helpers::data_to_hex(data, length).c_str());
}
// src, dest and offset are always in fixed positions
@@ -263,7 +264,7 @@ void RxService::add(uint8_t * data, uint8_t length) {
uint8_t dest = data[1] & 0x7F; // strip MSB, don't care if its read or write for processing
uint8_t offset = data[3]; // offset is always 4th byte
uint16_t type_id; // this could be 2 bytes for ems+
uint16_t type_id = 0; // this could be 2 bytes for ems+
uint8_t * message_data;
uint8_t message_length;
@@ -274,15 +275,21 @@ void RxService::add(uint8_t * data, uint8_t length) {
message_data = data + 4; // message block starts at 5th byte
message_length = length - 5; // remove 4 bytes header plus CRC
} else {
// EMS 2.0
// EMS 2.0 / EMS+
if (data[2] == 0xFF) {
type_id = (data[4] << 8) + data[5] + 256;
message_data = data + 6; // message block starts at 7th position
// check for empty data
// special broadcast telegrams on ems+ have no data values, some even don't have a type ID
if (length <= 7) {
message_length = 0; // special broadcast on ems+ have no data values
message_data = data; // bogus pointer, will not be used
message_length = 0;
if (length <= 5) {
type_id = 0; // has also an empty type_id
} else {
type_id = (data[4] << 8) + data[5] + 256;
}
} else {
message_length = length - 7; // remove 6 byte header plus CRC
message_data = data + 6; // message block starts at 7th position
}
} else {
// its F9 or F7
@@ -297,20 +304,28 @@ void RxService::add(uint8_t * data, uint8_t length) {
}
}
// if we don't have a type_id, exit
if (type_id == 0) {
return;
}
// create the telegram
auto telegram = std::make_shared<Telegram>(Telegram::Operation::RX, src, dest, type_id, offset, message_data, message_length);
// check if queue is full
if (rx_telegrams_.size() >= maximum_rx_telegrams_) {
if (rx_telegrams_.size() >= MAX_RX_TELEGRAMS) {
// rx_telegrams_overflow_ = true;
rx_telegrams_.pop_front();
}
// add to queue, with timestamp
DEBUG_LOG(F("New Rx [#%d] telegram added, length %d"), rx_telegram_id_, message_length);
// add to queue
LOG_DEBUG(F("New Rx [#%d] telegram, length %d"), rx_telegram_id_, message_length);
rx_telegrams_.emplace_back(rx_telegram_id_++, std::move(telegram));
}
//
// Tx CODE here
//
TxService::QueuedTxTelegram::QueuedTxTelegram(uint16_t id, std::shared_ptr<Telegram> && telegram)
: id_(id)
@@ -325,8 +340,6 @@ void TxService::flush_tx_queue() {
// start and initialize Tx
void TxService::start() {
// DEBUG_LOG(F("TxStart()"));
// grab the bus ID
Settings settings;
ems_bus_id(settings.ems_bus_id());
@@ -336,6 +349,19 @@ void TxService::start() {
read_request(EMSdevice::EMS_TYPE_UBADevices, EMSdevice::EMS_DEVICE_ID_BOILER);
}
// Tx loop
// here we check if the Tx is not full and report an error
void TxService::loop() {
#ifndef EMSESP_STANDALONE
if ((uuid::get_uptime() - last_tx_check_) > TX_LOOP_WAIT) {
last_tx_check_ = uuid::get_uptime();
if ((tx_telegrams_.size() >= MAX_TX_TELEGRAMS - 1) && (EMSbus::bus_connected())) {
LOG_ERROR(F("Tx buffer full. Looks like Tx is not working?"));
}
}
#endif
}
// sends a 1 byte poll which is our own device ID
void TxService::send_poll() {
EMSuart::send_poll(ems_bus_id() ^ ems_mask());
@@ -352,9 +378,8 @@ void TxService::send() {
// if there's nothing in the queue to send
// optionally, send back a poll and quit
// for now I've disabled the poll
if (tx_telegrams_.empty()) {
// send_poll();
// send_poll(); // TODO commented out poll for now. should add back when stable.
return;
}
@@ -431,11 +456,12 @@ void TxService::send_telegram(const QueuedTxTelegram & tx_telegram) {
length++; // add one since we want to now include the CRC
DEBUG_LOG(F("Sending %s Tx [#%d], telegram: %s"),
LOG_DEBUG(F("Sending %s Tx [#%d], telegram: %s"),
(telegram->operation == Telegram::Operation::TX_WRITE) ? F("write") : F("read"),
tx_telegram.id_,
telegram->to_string(telegram_raw, length).c_str());
// if we're watching an ID, then always show
if ((logger_.enabled(Level::TRACE))
&& ((telegram->src == EMSESP::trace_watch_id()) || (telegram->dest == EMSESP::trace_watch_id()) || (telegram->type_id == EMSESP::trace_watch_id()))) {
logger_.trace(F("Sending %s Tx [#%d], telegram: %s"),
@@ -446,9 +472,9 @@ void TxService::send_telegram(const QueuedTxTelegram & tx_telegram) {
// send the telegram to the UART Tx
EMSUART_STATUS status = EMSuart::transmit(telegram_raw, length);
LOG_TRACE(F("Tx: %s"), Helpers::data_to_hex(telegram_raw, length).c_str());
if (status != EMS_TX_STATUS_OK) {
logger_.err(F("Failed to transmit Tx via UART. Error: %s"), status == EMS_TX_WTD_TIMEOUT ? F("Timeout") : F("BRK"));
LOG_ERROR(F("Failed to transmit Tx via UART. Error: %s"), status == EMS_TX_WTD_TIMEOUT ? F("Timeout") : F("BRK"));
}
tx_waiting(true); // tx now in a wait state
@@ -465,12 +491,13 @@ void TxService::send_telegram(const uint8_t * data, const uint8_t length) {
}
telegram_raw[length] = calculate_crc(telegram_raw, length); // apppend CRC
DEBUG_LOG(F("Sending Raw telegram: %s (length=%d)"), Helpers::data_to_hex(telegram_raw, length).c_str(), length);
LOG_DEBUG(F("Sending Raw telegram: %s (length=%d)"), Helpers::data_to_hex(telegram_raw, length).c_str(), length);
// send the telegram to the UART Tx
EMSUART_STATUS status = EMSuart::transmit(telegram_raw, length);
LOG_TRACE(F("Tx: %s"), Helpers::data_to_hex(telegram_raw, length).c_str());
if (status != EMS_TX_STATUS_OK) {
logger_.err(F("Failed to transmit Tx via UART. Error: %s"), status == EMS_TX_WTD_TIMEOUT ? F("Timeout") : F("BRK"));
LOG_ERROR(F("Failed to transmit Tx via UART. Error: %s"), status == EMS_TX_WTD_TIMEOUT ? F("Timeout") : F("BRK"));
}
}
@@ -478,10 +505,10 @@ void TxService::send_telegram(const uint8_t * data, const uint8_t length) {
// given some details like the destination, type, offset and message block
void TxService::add(const uint8_t operation, const uint8_t dest, const uint16_t type_id, const uint8_t offset, uint8_t * message_data, const uint8_t message_length) {
auto telegram = std::make_shared<Telegram>(operation, ems_bus_id(), dest, type_id, offset, message_data, message_length);
DEBUG_LOG(F("New Tx [#%d] telegram added, length %d"), tx_telegram_id_, message_length);
LOG_DEBUG(F("New Tx [#%d] telegram, length %d"), tx_telegram_id_, message_length);
// if the queue is full, make room but removing the last one
if (tx_telegrams_.size() >= maximum_tx_telegrams_) {
if (tx_telegrams_.size() >= MAX_TX_TELEGRAMS) {
tx_telegrams_.pop_front();
}
@@ -493,7 +520,7 @@ void TxService::add(const uint8_t operation, const uint8_t dest, const uint16_t
void TxService::add(uint8_t * data, const uint8_t length) {
uint8_t message_length = length - 4;
if (!message_length) {
logger_.err(F("Bad Tx telegram, too short (message length is %d)"), message_length);
LOG_ERROR(F("Bad Tx telegram, too short (message length is %d)"), message_length);
return;
}
@@ -507,17 +534,17 @@ void TxService::add(uint8_t * data, const uint8_t length) {
auto telegram = std::make_shared<Telegram>(Telegram::Operation::TX_RAW, src, dest, type_id, offset, message_data, message_length);
// if the queue is full, make room but removing the last one
if (tx_telegrams_.size() >= maximum_tx_telegrams_) {
if (tx_telegrams_.size() >= MAX_TX_TELEGRAMS) {
tx_telegrams_.pop_front();
}
DEBUG_LOG(F("New Tx [#%d] telegram added, length %d"), tx_telegram_id_, message_length);
LOG_DEBUG(F("New Tx [#%d] telegram, length %d"), tx_telegram_id_, message_length);
tx_telegrams_.emplace_back(tx_telegram_id_++, std::move(telegram));
}
// send a Tx telegram to request data from an EMS device
void TxService::read_request(const uint16_t type_id, const uint8_t dest, const uint8_t offset) {
DEBUG_LOG(F("Tx read request to device 0x%02X for type ID 0x%02X"), dest, type_id);
LOG_DEBUG(F("Tx read request to device 0x%02X for type ID 0x%02X"), dest, type_id);
uint8_t message_data[1] = {EMS_MAX_TELEGRAM_LENGTH}; // request all data, 32 bytes
add(Telegram::Operation::TX_READ, dest, type_id, offset, message_data, 1);
@@ -582,7 +609,7 @@ uint8_t TxService::retry_tx() {
// and incoming Rx dest must be us (our ems_bus_id)
// for both src and dest we strip the MSB 8th bit
bool TxService::is_last_tx(const uint8_t src, const uint8_t dest) const {
// DEBUG_LOG(F("Comparing %02X=%02X , %02X,%02X"), (telegram_last_[1] & 0x7F), (src & 0x7F), (dest & 0x7F), ems_bus_id());
// LOG_DEBUG(F("Comparing %02X=%02X , %02X,%02X"), (telegram_last_[1] & 0x7F), (src & 0x7F), (dest & 0x7F), ems_bus_id());
return (((telegram_last_[1] & 0x7F) == (src & 0x7F)) && ((dest & 0x7F) == ems_bus_id()));
}
@@ -590,7 +617,7 @@ bool TxService::is_last_tx(const uint8_t src, const uint8_t dest) const {
void TxService::post_send_query() {
if (telegram_last_post_send_query_) {
uint8_t dest = (telegram_last_[1] & 0x7F);
DEBUG_LOG(F("Sending post validate read, type ID 0x%02X to dest 0x%02X"), telegram_last_post_send_query_, dest);
LOG_DEBUG(F("Sending post validate read, type ID 0x%02X to dest 0x%02X"), telegram_last_post_send_query_, dest);
read_request(telegram_last_post_send_query_, dest, 0); // no offset
}

View File

@@ -96,7 +96,7 @@ class EMSbus {
static bool bus_connected() {
#ifndef EMSESP_STANDALONE
if ((millis() - last_bus_activity_) > EMS_BUS_TIMEOUT) {
if ((uuid::get_uptime() - last_bus_activity_) > EMS_BUS_TIMEOUT) {
bus_connected_ = false;
}
return bus_connected_;
@@ -202,8 +202,6 @@ class RxService : public EMSbus {
static constexpr uint32_t RX_LOOP_WAIT = 800; // delay in processing Rx queue
uint32_t last_rx_check_ = 0;
size_t maximum_rx_telegrams_ = MAX_RX_TELEGRAMS;
// std::atomic<bool> rx_telegrams_overflow_{false};
uint8_t rx_telegram_id_ = 0; // queue counter
@@ -215,7 +213,7 @@ class RxService : public EMSbus {
class TxService : public EMSbus {
public:
static constexpr size_t MAX_TX_TELEGRAMS = 20;
static constexpr size_t MAX_TX_TELEGRAMS = 50;
static constexpr uint8_t TX_WRITE_FAIL = 4;
static constexpr uint8_t TX_WRITE_SUCCESS = 1;
@@ -224,6 +222,7 @@ class TxService : public EMSbus {
~TxService() = default;
void start();
void loop();
void send();
void add(const uint8_t operation, const uint8_t dest, const uint16_t type_id, const uint8_t offset, uint8_t * message_data, const uint8_t message_length);
@@ -292,9 +291,11 @@ class TxService : public EMSbus {
private:
static constexpr uint8_t MAXIMUM_TX_RETRIES = 3;
size_t maximum_tx_telegrams_ = MAX_TX_TELEGRAMS;
uint8_t tx_telegram_id_ = 0; // queue counter
static constexpr uint32_t TX_LOOP_WAIT = 10000; // when to check if Tx is up and running (10 sec)
uint32_t last_tx_check_ = 0;
std::deque<QueuedTxTelegram> tx_telegrams_;
uint8_t telegram_last_[EMS_MAX_TELEGRAM_LENGTH]; // copy of last telegram

View File

@@ -7,8 +7,8 @@ void EMSESP::run_test(uuid::console::Shell & shell, const std::string & command)
emsdevices.push_back(EMSFactory::add(EMSdevice::DeviceType::BOILER, EMSdevice::EMS_DEVICE_ID_BOILER, 0, "", "My Boiler", 0, 0));
// A fake response - UBADevices(0x07)
uint8_t t0[] = {0x08, 0x00, 0x07, 0x00, 0x0B, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47};
rxservice_.add(t0, sizeof(t0));
uint8_t t[] = {0x08, 0x00, 0x07, 0x00, 0x0B, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47};
rxservice_.add(t, sizeof(t));
return;
}
@@ -28,8 +28,8 @@ void EMSESP::run_test(uuid::console::Shell & shell, const std::string & command)
rxservice_.loop();
// simulate getting version information back from an unknown device
uint8_t u1[] = {0x09, 0x0B, 0x02, 0x00, 0x59, 0x01, 0x02, 0x56};
rxservice_.add(u1, sizeof(u1));
uint8_t t[] = {0x09, 0x0B, 0x02, 0x00, 0x59, 0x01, 0x02, 0x56};
rxservice_.add(t, sizeof(t));
rxservice_.loop();
return;
@@ -63,9 +63,9 @@ void EMSESP::run_test(uuid::console::Shell & shell, const std::string & command)
// SM100Monitor - type 0x0362 EMS+ - for SM100 and SM200
// B0 0B FF 00 02 62 00 44 02 7A 80 00 80 00 80 00 80 00 80 00 80 00 00 7C 80 00 80 00 80 00 80
uint8_t s1[] = {0xB0, 0x0B, 0xFF, 00, 0x02, 0x62, 00, 0x44, 0x02, 0x7A, 0x80, 00, 0x80, 0x00, 0x80, 00,
uint8_t t[] = {0xB0, 0x0B, 0xFF, 00, 0x02, 0x62, 00, 0x44, 0x02, 0x7A, 0x80, 00, 0x80, 0x00, 0x80, 00,
0x80, 00, 0x80, 00, 0x80, 00, 00, 0x7C, 0x80, 00, 0x80, 00, 0x80, 00, 0x80, 0x89};
rxservice_.add(s1, sizeof(s1));
rxservice_.add(t, sizeof(t));
rxservice_.loop();
shell.loop_all();
@@ -84,9 +84,9 @@ void EMSESP::run_test(uuid::console::Shell & shell, const std::string & command)
// RCPLUSStatusMessage_HC1(0x01A5)
// 98 00 FF 00 01 A5 00 CF 21 2E 00 00 2E 24 03 25 03 03 01 03 25 00 C8 00 00 11 01 03
uint8_t c1[] = {0x98, 0x00, 0xFF, 0x00, 0x01, 0xA5, 0x00, 0xCF, 0x21, 0x2E, 0x00, 0x00, 0x2E, 0x24, 0x03,
uint8_t t[] = {0x98, 0x00, 0xFF, 0x00, 0x01, 0xA5, 0x00, 0xCF, 0x21, 0x2E, 0x00, 0x00, 0x2E, 0x24, 0x03,
0x25, 0x03, 0x03, 0x01, 0x03, 0x25, 0x00, 0xC8, 0x00, 0x00, 0x11, 0x01, 0x03, 0x13};
rxservice_.add(c1, sizeof(c1));
rxservice_.add(t, sizeof(t));
rxservice_.loop();
shell.loop_all();
@@ -240,19 +240,19 @@ void EMSESP::run_test(uuid::console::Shell & shell, const std::string & command)
incoming_telegram(poll, 1);
// incoming Rx
uint8_t t17[] = {0x17, 0x08, 0x1A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3A};
incoming_telegram(t17, sizeof(t17));
uint8_t t1[] = {0x17, 0x08, 0x1A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3A};
incoming_telegram(t1, sizeof(t1));
rxservice_.loop();
// Simulate adding a Poll - should send retry
incoming_telegram(poll, 1);
show_emsbus(shell);
uint8_t t18[] = {0x21, 0x22};
send_write_request(0x91, 0x17, 0x00, t18, sizeof(t18), 0);
uint8_t t2[] = {0x21, 0x22};
send_write_request(0x91, 0x17, 0x00, t2, sizeof(t2), 0);
show_emsbus(shell);
incoming_telegram(t17, sizeof(t17));
incoming_telegram(t1, sizeof(t1));
txservice_.flush_tx_queue();
@@ -324,9 +324,17 @@ void EMSESP::run_test(uuid::console::Shell & shell, const std::string & command)
if (command == "rx2") {
// incoming Rx
uint8_t t71[] = {0x1B, 0x5B, 0xFD, 0x2D, 0x9E, 0x3A, 0xB6, 0xE5, 0x02, 0x20, 0x33, 0x30, 0x32, 0x3A, 0x20, 0x5B, 0x73,
uint8_t t[] = {0x1B, 0x5B, 0xFD, 0x2D, 0x9E, 0x3A, 0xB6, 0xE5, 0x02, 0x20, 0x33, 0x30, 0x32, 0x3A, 0x20, 0x5B, 0x73,
0xFF, 0xFF, 0xCB, 0xDF, 0xB7, 0xA7, 0xB5, 0x67, 0x77, 0x77, 0xE4, 0xFF, 0xFD, 0x77, 0xFF, 0xD1};
incoming_telegram(t71, sizeof(t71));
incoming_telegram(t, sizeof(t));
return;
}
// https://github.com/proddy/EMS-ESP/issues/380#issuecomment-633663007
if (command == "rx3") {
// incoming Rx
uint8_t t[] = {0x21, 0x0B, 0xFF, 0x00, 0xDA};
incoming_telegram(t, sizeof(t));
return;
}
@@ -341,8 +349,8 @@ void EMSESP::run_test(uuid::console::Shell & shell, const std::string & command)
// testing the UART tx command, without a queue
if (command == "tx2") {
uint8_t tx[] = {0x0B, 0x88, 0x18, 0x00, 0x20, 0xD4}; // including CRC
EMSuart::transmit(tx, sizeof(tx));
uint8_t t[] = {0x0B, 0x88, 0x18, 0x00, 0x20, 0xD4}; // including CRC
EMSuart::transmit(t, sizeof(t));
return;
}

View File

@@ -130,10 +130,10 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i
if (((num_devices == 1) && (actual_master_thermostat == EMSESP_DEFAULT_MASTER_THERMOSTAT) && ((device_id == 0x10) || (device_id == 0x17)))
|| (master_thermostat == device_id)) {
EMSESP::actual_master_thermostat(device_id);
DEBUG_LOG(F("Registering new thermostat with device ID 0x%02X (as the master)"), device_id);
LOG_DEBUG(F("Registering new thermostat with device ID 0x%02X (as the master)"), device_id);
init_mqtt();
} else {
DEBUG_LOG(F("Registering new thermostat with device ID 0x%02X"), device_id);
LOG_DEBUG(F("Registering new thermostat with device ID 0x%02X"), device_id);
}
}
@@ -145,29 +145,32 @@ void Thermostat::init_mqtt() {
// for each of the heating circuits
if (mqtt_format_ == Settings::MQTT_format::HA) {
for (uint8_t hc = 0; hc < monitor_typeids.size(); hc++) {
std::string topic(100, '\0'); // e.g homeassistant/climate/hc1/thermostat/config
snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/climate/hc%d/thermostat/config"), hc + 1);
// Mqtt::publish(topic.c_str()); // empty payload, this remove any previous config sent to HA
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_MEDIUM> doc;
std::string payload(100, '\0');
snprintf_P(&payload[0], payload.capacity() + 1, PSTR("thermostat_hc%d"), hc + 1);
doc["name"] = payload; // "name": "thermostat_hc1"
doc["unique_id"] = payload; // "unique_id": "thermostat_hc1"
snprintf_P(&payload[0], payload.capacity() + 1, PSTR("homeassistant/climate/hc%d/thermostat"), hc + 1);
doc["~"] = payload; // "homeassistant/climate/hc1/thermostat"
std::string hc_text(10, '\0');
snprintf_P(&hc_text[0], hc_text.capacity() + 1, PSTR("hc%d"), hc + 1);
doc["name"] = hc_text;
doc["uniq_id"] = hc_text;
doc["~"] = "homeassistant/climate/ems-esp";
doc["mode_cmd_t"] = "~/cmd_mode";
doc["mode_stat_t"] = "~/state";
doc["mode_stat_tpl"] = "{{value_json.mode}}";
doc["temp_cmd_t"] = "~/cmd_temp";
doc["temp_stat_t"] = "~/state";
doc["temp_stat_tpl"] = "{{value_json.seltemp}}";
doc["curr_temp_t"] = "~/state";
doc["curr_temp_tpl"] = "{{value_json.currtemp}}";
std::string mode_str(30, '\0');
snprintf_P(&mode_str[0], 30, PSTR("{{value_json.hc%d.mode}}"), hc + 1);
doc["mode_stat_tpl"] = mode_str;
std::string seltemp_str(30, '\0');
snprintf_P(&seltemp_str[0], 30, PSTR("{{value_json.hc%d.seltemp}}"), hc + 1);
doc["temp_stat_tpl"] = seltemp_str;
std::string currtemp_str(30, '\0');
snprintf_P(&currtemp_str[0], 30, PSTR("{{value_json.hc%d.currtemp}}"), hc + 1);
doc["curr_temp_tpl"] = currtemp_str;
doc["min_temp"] = "5";
doc["max_temp"] = "40";
doc["temp_step"] = "0.5";
@@ -177,7 +180,10 @@ void Thermostat::init_mqtt() {
modes.add("heat");
modes.add("auto");
Mqtt::publish(topic.c_str(), doc, true); // publish the config payload with retain flag
std::string topic(100, '\0'); // e.g homeassistant/climate/hc1/thermostat/config
snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/climate/ems-esp/hc%d/config"), hc + 1);
// Mqtt::publish(topic); // empty payload, this remove any previous config sent to HA
Mqtt::publish(topic, doc, true); // publish the config payload with retain flag
// subscribe to the temp and mode commands
snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/climate/hc%d/thermostat/cmd_temp"), hc + 1);
@@ -212,7 +218,7 @@ void Thermostat::thermostat_cmd(const char * message) {
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
DeserializationError error = deserializeJson(doc, message);
if (error) {
DEBUG_LOG(F("MQTT error: payload %s, error %s"), message, error.c_str());
LOG_DEBUG(F("MQTT error: payload %s, error %s"), message, error.c_str());
return;
}
@@ -342,7 +348,7 @@ void Thermostat::publish_values() {
return;
}
DEBUG_LOG(F("Performing a thermostat publish (device ID 0x%02X)"), device_id());
LOG_DEBUG(F("Performing a thermostat publish (device ID 0x%02X)"), device_id());
uint8_t flags = (this->flags() & 0x0F); // specific thermostat characteristics, stripping the option bits
bool has_data = false;
@@ -352,27 +358,28 @@ void Thermostat::publish_values() {
JsonObject dataThermostat;
// optional, add external temp. I don't think anyone actually is interested in this
if ((flags == EMS_DEVICE_FLAG_RC35) && (mqtt_format_ == Settings::MQTT_format::SINGLE)) {
if ((flags == EMS_DEVICE_FLAG_RC35) && ((mqtt_format_ == Settings::MQTT_format::SINGLE) || (mqtt_format_ == Settings::MQTT_format::MY))) {
if (dampedoutdoortemp != EMS_VALUE_INT_NOTSET) {
doc["dampedtemp"] = dampedoutdoortemp;
rootThermostat["dampedtemp"] = dampedoutdoortemp;
}
if (tempsensor1 != EMS_VALUE_USHORT_NOTSET) {
doc["tempsensor1"] = (float)tempsensor1 / 10;
rootThermostat["tempsens1"] = (float)tempsensor1 / 10;
}
if (tempsensor2 != EMS_VALUE_USHORT_NOTSET) {
doc["tempsensor1"] = (float)tempsensor2 / 10;
rootThermostat["tempsens2"] = (float)tempsensor2 / 10;
}
}
// go through all the heating circuits
for (const auto & hc : heating_circuits_) {
if ((hc->setpoint_roomTemp == EMS_VALUE_SHORT_NOTSET) || (hc->curr_roomTemp == EMS_VALUE_SHORT_NOTSET)) {
break; // skip this HC as we don't have the temperature values yet
// if ((hc->setpoint_roomTemp == EMS_VALUE_SHORT_NOTSET) || (hc->curr_roomTemp == EMS_VALUE_SHORT_NOTSET)) {
if (hc->heatingtype == 0) {
break; // skip this HC
}
has_data = true;
// if the MQTT format is 'nested' then create the parent object hc<n>
if (mqtt_format_ == Settings::MQTT_format::NESTED) {
// if the MQTT format is 'nested' or 'ha' then create the parent object hc<n>
if (mqtt_format_ != Settings::MQTT_format::SINGLE) {
// create nested json for each HC
char hc_name[10]; // hc{1-4}
strlcpy(hc_name, "hc", 10);
@@ -400,7 +407,7 @@ void Thermostat::publish_values() {
if (hc->setpoint_roomTemp != EMS_VALUE_SHORT_NOTSET) {
dataThermostat["seltemp"] = Helpers::round2((float)hc->setpoint_roomTemp / setpoint_temp_divider);
}
if (hc->curr_roomTemp != EMS_VALUE_SHORT_NOTSET) {
if (hc->curr_roomTemp != EMS_VALUE_SHORT_NOTSET && hc->curr_roomTemp != EMS_VALUE_USHORT_NOTSET) {
dataThermostat["currtemp"] = Helpers::round2((float)hc->curr_roomTemp / curr_temp_divider);
}
@@ -448,10 +455,10 @@ void Thermostat::publish_values() {
// special handling of mode type, for the RC35 replace with summer/holiday
// https://github.com/proddy/EMS-ESP/issues/373#issuecomment-619810209
if ((flags & 0x0F) == EMS_DEVICE_FLAG_RC35) {
if (hc->holiday_mode != EMS_VALUE_UINT_NOTSET) {
dataThermostat["modetype"] = F("holiday");
} else if (hc->summer_mode != EMS_VALUE_UINT_NOTSET) {
if (hc->summer_mode) {
dataThermostat["modetype"] = F("summer");
} else if (hc->holiday_mode) {
dataThermostat["modetype"] = F("holiday");
} else if (hc->mode_type != EMS_VALUE_UINT_NOTSET) {
dataThermostat["modetype"] = mode_tostring(hc->get_mode_type(flags));
}
@@ -459,9 +466,7 @@ void Thermostat::publish_values() {
dataThermostat["modetype"] = mode_tostring(hc->get_mode_type(flags));
}
// if format is single, send immediately
// if its HA send it to the special topic
// if format is single, send immediately and quit
if (mqtt_format_ == Settings::MQTT_format::SINGLE) {
char topic[30];
char s[3]; // for formatting strings
@@ -469,16 +474,19 @@ void Thermostat::publish_values() {
strlcat(topic, Helpers::itoa(s, hc->hc_num()), 30); // append hc to topic
Mqtt::publish(topic, doc);
return;
} else if (mqtt_format_ == Settings::MQTT_format::HA) {
std::string topic(100, '\0');
snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/climate/hc%d/thermostat/state"), hc->hc_num());
Mqtt::publish(topic.c_str(), doc);
return;
}
}
if (!has_data) {
return; // nothing to send
}
// if we're using nested json, send all in one go
if ((mqtt_format_ == Settings::MQTT_format::NESTED) && has_data) {
if (mqtt_format_ == Settings::MQTT_format::NESTED) {
Mqtt::publish("thermostat_data", doc);
} else if (mqtt_format_ == Settings::MQTT_format::HA) {
Mqtt::publish("homeassistant/climate/ems-esp/state", doc);
} else if (mqtt_format_ == Settings::MQTT_format::MY) {
Mqtt::publish("thermostat_data", doc);
}
}
@@ -635,9 +643,9 @@ std::string Thermostat::mode_tostring(uint8_t mode) const {
case HeatingCircuit::Mode::HEAT:
return read_flash_string(F("heat"));
break;
case HeatingCircuit::Mode::HOLIDAY:
return read_flash_string(F("holiday"));
break;
// case HeatingCircuit::Mode::HOLIDAY:
// return read_flash_string(F("holiday"));
// break;
case HeatingCircuit::Mode::NOFROST:
return read_flash_string(F("nofrost"));
break;
@@ -934,11 +942,7 @@ void Thermostat::process_RC35Monitor(std::shared_ptr<const Telegram> telegram) {
std::shared_ptr<Thermostat::HeatingCircuit> hc = heating_circuit(telegram);
// ignore if the value is 0 (see https://github.com/proddy/EMS-ESP/commit/ccc30738c00f12ae6c89177113bd15af9826b836)
if (telegram->message_data[2] != 0x00) {
telegram->read_value8(hc->setpoint_roomTemp, 2); // is * 2, force to single byte
}
telegram->read_value(hc->curr_roomTemp, 3); // is * 10 - or 0x7D00 if thermostat is mounted on boiler
telegram->read_value(hc->mode_type, 1, 1);
telegram->read_value(hc->summer_mode, 1, 0);
@@ -998,19 +1002,19 @@ void Thermostat::process_RCTime(std::shared_ptr<const Telegram> telegram) {
// 0xA5 - Set the display settings
void Thermostat::set_settings_display(const uint8_t ds) {
logger_.info(F("Setting display to %d"), ds);
LOG_INFO(F("Setting display to %d"), ds);
write_command(EMS_TYPE_IBASettings, 0, ds);
}
// 0xA5 - Set the building settings
void Thermostat::set_settings_building(const uint8_t bg) {
logger_.info(F("Setting building to %d"), bg);
LOG_INFO(F("Setting building to %d"), bg);
write_command(EMS_TYPE_IBASettings, 6, bg);
}
// 0xA5 Set the language settings
void Thermostat::set_settings_language(const uint8_t lg) {
logger_.info(F("Setting building to %d"), lg);
LOG_INFO(F("Setting building to %d"), lg);
write_command(EMS_TYPE_IBASettings, 1, lg);
}
@@ -1037,21 +1041,21 @@ void Thermostat::set_mode(const std::string & mode, const uint8_t hc_num) {
} else if (mode_tostring(HeatingCircuit::Mode::COMFORT) == mode) {
set_mode(HeatingCircuit::Mode::COMFORT, hc_num);
} else {
logger_.warning(F("Invalid mode %s. Cannot set"), mode.c_str());
LOG_WARNING(F("Invalid mode %s. Cannot set"), mode.c_str());
}
}
// Set the thermostat working mode
void Thermostat::set_mode(const uint8_t mode, const uint8_t hc_num) {
if (can_write()) {
logger_.warning(F("Write not supported for this model Thermostat"));
LOG_WARNING(F("Write not supported for this model Thermostat"));
return;
}
// get hc based on number
std::shared_ptr<Thermostat::HeatingCircuit> hc = heating_circuit(hc_num);
if (hc == nullptr) {
logger_.warning(F("set mode: Heating Circuit %d not found or activated"), hc_num);
LOG_WARNING(F("set mode: Heating Circuit %d not found or activated"), hc_num);
return;
}
@@ -1127,7 +1131,7 @@ void Thermostat::set_mode(const uint8_t mode, const uint8_t hc_num) {
break;
}
logger_.info(F("Setting thermostat mode to %s for heating circuit %d"), mode_tostring(mode).c_str(), hc->hc_num());
LOG_INFO(F("Setting thermostat mode to %s for heating circuit %d"), mode_tostring(mode).c_str(), hc->hc_num());
// add the write command to the Tx queue
// post validate is the corresponding monitor or set type IDs as they can differ per model
@@ -1138,14 +1142,14 @@ void Thermostat::set_mode(const uint8_t mode, const uint8_t hc_num) {
// Set the temperature of the thermostat
void Thermostat::set_temperature(const float temperature, const uint8_t mode, const uint8_t hc_num) {
if (can_write()) {
logger_.warning(F("Write not supported for this model Thermostat"));
LOG_WARNING(F("Write not supported for this model Thermostat"));
return;
}
// get hc based on number
std::shared_ptr<Thermostat::HeatingCircuit> hc = heating_circuit(hc_num);
if (hc == nullptr) {
logger_.warning(F("set temperature: Heating Circuit %d not found or activated for device ID 0x%02X"), hc_num, device_id());
LOG_WARNING(F("set temperature: Heating Circuit %d not found or activated for device ID 0x%02X"), hc_num, device_id());
return;
}
@@ -1244,7 +1248,7 @@ void Thermostat::set_temperature(const float temperature, const uint8_t mode, co
// if we know what to send and to where, go and do it
if (offset != -1) {
char s[10];
logger_.info(F("Setting thermostat temperature to %s for heating circuit %d, mode %s"),
LOG_INFO(F("Setting thermostat temperature to %s for heating circuit %d, mode %s"),
Helpers::render_value(s, temperature, 2),
hc->hc_num(),
mode_tostring(mode).c_str());

View File

@@ -16,7 +16,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* ESP32 UART port by Arwed Richert @ArwedL */
/*
* ESP32 UART port by @ArwedL and improved by @MichaelDvP. See https://github.com/proddy/EMS-ESP/issues/380
*/
#if defined(ESP32)
@@ -30,10 +32,10 @@ static intr_handle_t uart_handle;
static RingbufHandle_t buf_handle = NULL;
static uint8_t rxbuf[UART_FIFO_LEN];
static uint8_t rxlen;
bool drop_first_rx = true;
/*
* Task to handle the incoming data.
* Task to handle the incoming data
*/
void EMSuart::emsuart_recvTask(void * param) {
while (1) {
@@ -50,17 +52,17 @@ void EMSuart::emsuart_recvTask(void * param) {
/*
* UART interrupt, on break read the fifo and put the whole telegram to ringbuffer
*/
static void IRAM_ATTR uart_intr_handle(void *arg)
{
static void IRAM_ATTR uart_intr_handle(void * arg) {
if (EMS_UART.int_st.brk_det) {
uint8_t rx_fifo_len = EMS_UART.status.rxfifo_cnt;
for (rxlen = 0; rxlen < rx_fifo_len; rxlen++) {
rxbuf[rxlen] = EMS_UART.fifo.rw_byte; // read all bytes into buffer
}
if ((rxlen == 2) || ((rxlen > 4) && (rxlen <= EMS_MAXBUFFERSIZE))) {
if (!drop_first_rx && (rxlen == 2) || ((rxlen > 4) && (rxlen <= EMS_MAXBUFFERSIZE))) {
int baseType = 0;
xRingbufferSendFromISR(buf_handle, rxbuf, rxlen - 1, &baseType);
}
drop_first_rx = false;
EMS_UART.int_clr.brk_det = 1; // clear flag
EMS_UART.conf0.txd_brk = 0; // if it was break from sending, clear bit
}
@@ -76,30 +78,35 @@ void EMSuart::start(uint8_t tx_mode) {
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
};
ESP_ERROR_CHECK(uart_param_config(EMSUART_UART, &uart_config));
ESP_ERROR_CHECK(uart_set_pin(EMSUART_UART, EMSUART_TXPIN, EMSUART_RXPIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
//EMS_UART.conf1.rxfifo_full_thrhd = 127; // enough to hold the incomming telegram, should never reached
//EMS_UART.idle_conf.tx_brk_num = 12; // breaklenght 12 bit
EMS_UART.int_ena.val = 0; // diable all intr.
//EMS_UART.conf1.rxfifo_full_thrhd = 127; // enough to hold the incoming telegram, should never reached
//EMS_UART.idle_conf.tx_brk_num = 12; // breaklength 12 bit
EMS_UART.int_ena.val = 0; // disable all intr.
EMS_UART.int_clr.val = 0xFFFFFFFF; // clear all intr. flags
EMS_UART.int_ena.brk_det = 1; // activate only break
buf_handle = xRingbufferCreate(128, RINGBUF_TYPE_NOSPLIT);
ESP_ERROR_CHECK(uart_isr_register(EMSUART_UART, uart_intr_handle, NULL, ESP_INTR_FLAG_IRAM, &uart_handle));
xTaskCreate(emsuart_recvTask, "emsuart_recvTask", 2048, NULL, 12, NULL);
drop_first_rx = true;
EMS_UART.int_ena.brk_det = 1; // activate only break
}
/*
* Stop, disables interrupt
*/
void EMSuart::stop() {
EMS_UART.int_ena.val = 0; //diable all intr.
EMS_UART.int_ena.val = 0; // disable all intr.
};
/*
* Restart Interrupt
*/
void EMSuart::restart() {
EMS_UART.int_clr.val = 0xFFFFFFFF; // clear all intr. flags
if (EMS_UART.int_st.brk_det) {
EMS_UART.int.clr.brk_det = 1; // clear break if happend
drop_first_rx = true; // and drop first frame
}
EMS_UART.int_ena.brk_det = 1; // activate only break
};
@@ -109,7 +116,7 @@ void EMSuart::restart(){
void EMSuart::send_poll(uint8_t data) {
EMS_UART.conf0.txd_brk = 0; // just to make sure the bit is cleared
EMS_UART.fifo.rw_byte = data;
EMS_UART.idle_conf.tx_brk_num = 12; // breaklenght 12 bit
EMS_UART.idle_conf.tx_brk_num = 12; // breaklength 12 bit
EMS_UART.conf0.txd_brk = 1; // sending ends in a break
}
@@ -128,7 +135,7 @@ EMSUART_STATUS EMSuart::transmit(uint8_t * buf, uint8_t len) {
EMS_UART.fifo.rw_byte = buf[i];
}
//uart_tx_chars(EMSUART_UART, (const char *)buf, len);
EMS_UART.idle_conf.tx_brk_num = 12; // breaklenght 12 bit
EMS_UART.idle_conf.tx_brk_num = 12; // breaklength 12 bit
EMS_UART.conf0.txd_brk = 1; // sending ends in a break
}
return EMS_TX_STATUS_OK;

View File

@@ -27,15 +27,16 @@
#include "freertos/queue.h"
#include <driver/uart.h>
#define EMS_MAXBUFFERSIZE 33 // max size of the buffer. EMS packets are max 32 bytes, plus BRK
#define EMSUART_UART UART_NUM_2 // UART 0 --> Changed to 2 for ESP32 // To do: Adapt
#define EMS_MAXBUFFERSIZE 33 // max size of the buffer. EMS packets are max 32 bytes, plus extra 2 for BRKs
#define EMSUART_UART UART_NUM_2 // on the ESP32 we're using UART2
#define EMS_UART UART2 // for intr setting
//#define EMSUART_RXPIN 17 // To do: Adapt seems to be IO17 for ESP32 UART2 RX pin
//#define EMSUART_TXPIN 16 // To do: Adapt seems to be IO16 for ESP32 UART2 TX pin
#define EMSUART_RXPIN 23 // Wemos D1 ESP32 UART2 RX pin for compatibility
#define EMSUART_TXPIN 5 // Wemos D1 ESP32 UART2 TX pin for compatibility
#define EMSUART_BAUD 9600 // uart baud rate for the EMS circuit
// customize the GPIO pins for RX and TX here
#define EMSUART_RXPIN 23 // 17 is UART2 RX. Use 23 for D7 on a Wemos D1-32 mini for backwards compatabilty
#define EMSUART_TXPIN 5 // 16 is UART2 TX. Use 5 for D8 on a Wemos D1-32 mini for backwards compatabilty
namespace emsesp {
typedef enum {

View File

@@ -25,15 +25,15 @@
namespace emsesp {
os_event_t recvTaskQueue[EMSUART_recvTaskQueueLen]; // our Rx queue
EMSuart::EMSRxBuf_t * pEMSRxBuf;
EMSuart::EMSRxBuf_t * paEMSRxBuf[EMS_MAXBUFFERS];
uint8_t emsRxBufIdx = 0;
bool drop_first_rx = true;
uint8_t phantomBreak = 0;
uint8_t tx_mode_ = EMS_TXMODE_NEW;
//
// Main interrupt handler
// Important: must not use ICACHE_FLASH_ATTR
//
void ICACHE_RAM_ATTR EMSuart::emsuart_rx_intr_handler(void * para) {
static uint8_t length = 0;
static uint8_t uart_buffer[128];
@@ -46,12 +46,13 @@ void ICACHE_RAM_ATTR EMSuart::emsuart_rx_intr_handler(void * para) {
}
USIE(EMSUART_UART) = 0; // disable all interrupts and clear them
USC0(EMSUART_UART) &= ~(1 << UCBRK); // reset <BRK> from sending
if (length < EMS_MAXBUFFERSIZE) { // only a valid telegram
if (!drop_first_rx && (length < EMS_MAXBUFFERSIZE)) { // only a valid telegram
pEMSRxBuf->length = length;
os_memcpy((void *)pEMSRxBuf->buffer, (void *)&uart_buffer, pEMSRxBuf->length); // copy data into transfer buffer, including the BRK 0x00 at the end
system_os_post(EMSUART_recvTaskPrio, 0, 0); // call emsuart_recvTask() at next opportunity
}
USIC(EMSUART_UART) = (1 << UIBD); // INT clear the BREAK detect interrupt
drop_first_rx = false;
USIC(EMSUART_UART) |= (1 << UIBD); // INT clear the BREAK detect interrupt
USIE(EMSUART_UART) = (1 << UIBD); // enable only rx break
}
}
@@ -67,6 +68,11 @@ void ICACHE_FLASH_ATTR EMSuart::emsuart_recvTask(os_event_t * events) {
uint8_t length = pCurrent->length; // number of bytes including the BRK at the end
pCurrent->length = 0;
if (phantomBreak) {
phantomBreak = 0;
length--; // remove phantom break from Rx buffer
}
// it's a poll or status code, single byte and ok to send on, then quit
if (length == 2) {
EMSESP::incoming_telegram((uint8_t *)pCurrent->buffer, 1);
@@ -74,7 +80,7 @@ void ICACHE_FLASH_ATTR EMSuart::emsuart_recvTask(os_event_t * events) {
}
// also telegrams with no data value
// then transmit EMS buffer, excluding the BRK, lenght is checked by irq
// then transmit EMS buffer, excluding the BRK, length is checked by irq
if (length > 4) {
EMSESP::incoming_telegram((uint8_t *)pCurrent->buffer, length - 1);
}
@@ -84,7 +90,7 @@ void ICACHE_FLASH_ATTR EMSuart::emsuart_recvTask(os_event_t * events) {
* init UART0 driver
*/
void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) {
tx_mode_ = tx_mode;
// allocate and preset EMS Receive buffers
for (int i = 0; i < EMS_MAXBUFFERS; i++) {
EMSRxBuf_t * p = (EMSRxBuf_t *)malloc(sizeof(EMSRxBuf_t));
@@ -92,9 +98,6 @@ void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) {
}
pEMSRxBuf = paEMSRxBuf[0]; // reset EMS Rx Buffer
//ETS_UART_INTR_DISABLE();
//ETS_UART_INTR_ATTACH(nullptr, nullptr);
// pin settings
PIN_PULLUP_DIS(PERIPHS_IO_MUX_U0TXD_U);
PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0TXD);
@@ -103,24 +106,22 @@ void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) {
// set 9600, 8 bits, no parity check, 1 stop bit
USD(EMSUART_UART) = (UART_CLK_FREQ / EMSUART_BAUD);
USC0(EMSUART_UART) = EMSUART_CONFIG; // 8N1
USC0(EMSUART_UART) = EMSUART_CONFIG;
// flash fifo buffers, not required since we drop the first telegram
// USC0(EMSUART_UART) |= ((1 << UCRXRST) | (1 << UCTXRST)); // set bits
// USC0(EMSUART_UART) &= ~((1 << UCRXRST) | (1 << UCTXRST)); // clear bits
USC0(EMSUART_UART) |= ((1 << UCRXRST) | (1 << UCTXRST)); // set bits
USC0(EMSUART_UART) &= ~((1 << UCRXRST) | (1 << UCTXRST)); // clear bits
// UCFFT = RX FIFO Full Threshold (7 bit) = want this to be more than 32)
USC1(EMSUART_UART) = (0x7F << UCFFT); // rx buffer full
// we dont use fifo-full interrupt anymore, no need to set this
//USC1(EMSUART_UART) = (0x7F << UCFFT); // rx buffer full
USIE(EMSUART_UART) = 0; // disable all interrupts
USIC(EMSUART_UART) = 0xFFFF; // clear all interupts
// set up interrupt callbacks for Rx
system_os_task(emsuart_recvTask, EMSUART_recvTaskPrio, recvTaskQueue, EMSUART_recvTaskQueueLen);
// disable esp debug which will go to Tx and mess up the line - see https://github.com/espruino/Espruino/issues/655
system_set_os_print(0);
// swap Rx and Tx pins to use GPIO13 (D7) and GPIO15 (D8) respectively
system_uart_swap();
USIC(EMSUART_UART) = 0xFFFF; // clear all interupt flags
system_set_os_print(0); // disable esp debug which will go to Tx and mess up the line - see https://github.com/espruino/Espruino/issues/655
system_uart_swap(); // swap Rx and Tx pins to use GPIO13 (D7) and GPIO15 (D8) respectively
system_os_task(emsuart_recvTask, EMSUART_recvTaskPrio, recvTaskQueue, EMSUART_recvTaskQueueLen); // set up interrupt callbacks for Rx
ETS_UART_INTR_ATTACH(emsuart_rx_intr_handler, nullptr);
drop_first_rx = true; // drop first telegram since it is incomplete
USIE(EMSUART_UART) = (1 << UIBD); // enable only rx break interrupt
//ETS_UART_INTR_ENABLE();
}
/*
@@ -128,20 +129,30 @@ void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) {
* This is called prior to an OTA upload and also before a save to the filesystem to prevent conflicts
*/
void ICACHE_FLASH_ATTR EMSuart::stop() {
USIE(EMSUART_UART) = 0; // disable interrup
USIE(EMSUART_UART) = 0; // disable uart interrupt
}
/*
* re-start UART0 driver
*/
void ICACHE_FLASH_ATTR EMSuart::restart() {
USIE(EMSUART_UART) = (1 << UIBD); // enable only rx break
if (USIS(EMSUART_UART) & ((1 << UIBD))) { // if we had a break
USIC(EMSUART_UART) |= (1 << UIBD); // clear the BREAK detect flag
drop_first_rx = true; // and drop first frame
} // otherwise there is the beginning of a valid telegram in the fifo
USIE(EMSUART_UART) = (1 << UIBD); // enable rx break interrupt
}
void EMSuart::send_poll(uint8_t data) {
USC0(EMSUART_UART) &= ~(1 << UCBRK); // clear <BRK> bit
if (tx_mode_ == EMS_TXMODE_NEW) {
USC0(EMSUART_UART) &= ~(1 << UCBRK); // in doubt clear <BRK> bit
USF(EMSUART_UART) = data;
USC0(EMSUART_UART) |= (1 << UCBRK); // send <BRK> at the end
} else {
USF(EMSUART_UART) = data;
delayMicroseconds(EMSUART_TX_BRK_WAIT);
tx_brk(); // send <BRK>
}
}
/*
@@ -149,15 +160,166 @@ void EMSuart::send_poll(uint8_t data) {
* buf contains the CRC and len is #bytes including the CRC
*/
EMSUART_STATUS ICACHE_FLASH_ATTR EMSuart::transmit(uint8_t * buf, uint8_t len) {
if (len) {
if (len == 0) {
return EMS_TX_STATUS_OK; // nothing to send
}
// new code from Michael. See https://github.com/proddy/EMS-ESP/issues/380
if (tx_mode_ == EMS_TXMODE_NEW) {
if ((USS(EMSUART_UART) >> USTXC) & 0xFF) { // buffer not empty
return EMS_TX_WTD_TIMEOUT;
}
USC0(EMSUART_UART) &= ~(1 << UCBRK); // clear <BRK> bit
for (uint8_t i = 0; i < len; i++) {
USF(EMSUART_UART) = buf[i];
USF(EMSUART_UART) = buf[i]; // fill fifo buffer
}
USC0(EMSUART_UART) |= (1 << UCBRK); // send <BRK> at the end
}
return EMS_TX_STATUS_OK;
}
// EMS+ https://github.com/proddy/EMS-ESP/issues/23#
if (tx_mode_ == EMS_TXMODE_EMSPLUS) { // With extra tx delay for EMS+
for (uint8_t i = 0; i < len; i++) {
USF(EMSUART_UART) = buf[i];
delayMicroseconds(EMSUART_TX_BRK_WAIT);
}
tx_brk(); // send <BRK>
return EMS_TX_STATUS_OK;
}
// Junkers logic by @philrich
if (tx_mode_ == EMS_TXMODE_HT3) {
for (uint8_t i = 0; i < len; i++) {
USF(EMSUART_UART) = buf[i];
// just to be safe wait for tx fifo empty (still needed?)
while (((USS(EMSUART_UART) >> USTXC) & 0xff))
;
// wait until bits are sent on wire
delayMicroseconds(EMSUART_TX_WAIT_BYTE - EMSUART_TX_LAG + EMSUART_TX_WAIT_GAP); // 1760
}
tx_brk(); // send <BRK>
return EMS_TX_STATUS_OK;
}
/*
* Logic for tx_mode of 0 (EMS_TXMODE_DEFAULT)
* based on code from https://github.com/proddy/EMS-ESP/issues/103 by @susisstrolch
*
* Logic:
* we emit the whole telegram, with Rx interrupt disabled, collecting busmaster response in FIFO.
* after sending the last char we poll the Rx status until either
* - size(Rx FIFO) == size(Tx-Telegram)
* - <BRK> is detected
* At end of receive we re-enable Rx-INT and send a Tx-BRK in loopback mode.
*
* EMS-Bus error handling
* 1. Busmaster stops echoing on Tx w/o permission
* 2. Busmaster cancel telegram by sending a BRK
*
* Case 1. is handled by a watchdog counter which is reset on each
* Tx attempt. The timeout should be 20x EMSUART_BIT_TIME plus
* some smart guess for processing time on targeted EMS device.
* We set Status to EMS_TX_WTD_TIMEOUT and return
*
* Case 2. is handled via a BRK chk during transmission.
* We set Status to EMS_TX_BRK_DETECT and return
*
*/
EMSUART_STATUS result = EMS_TX_STATUS_OK;
// disable rx interrupt
// clear Rx status register, resetting the Rx FIFO and flush it
ETS_UART_INTR_DISABLE();
USC0(EMSUART_UART) |= (1 << UCRXRST);
emsuart_flush_fifos();
// send the bytes along the serial line
for (uint8_t i = 0; i < len; i++) {
uint16_t wdc = EMS_TX_TO_COUNT; // 1760
volatile uint8_t _usrxc = (USS(EMSUART_UART) >> USRXC) & 0xFF;
USF(EMSUART_UART) = buf[i]; // send each Tx byte
// wait for echo from the busmaster
while (((USS(EMSUART_UART) >> USRXC) & 0xFF) == _usrxc) {
delayMicroseconds(EMSUART_BUSY_WAIT); // burn CPU cycles...
if (--wdc == 0) {
ETS_UART_INTR_ENABLE();
return EMS_TX_WTD_TIMEOUT;
}
if (USIR(EMSUART_UART) & (1 << UIBD)) {
USIC(EMSUART_UART) = (1 << UIBD); // clear BRK detect IRQ
ETS_UART_INTR_ENABLE();
return EMS_TX_BRK_DETECT;
}
}
}
// we got the whole telegram in the Rx buffer
// on Rx-BRK (bus collision), we simply enable Rx and leave it
// otherwise we send the final Tx-BRK in the loopback and re=enable Rx-INT.
// worst case, we'll see an additional Rx-BRK...
if (result == EMS_TX_STATUS_OK) {
// neither bus collision nor timeout - send terminating BRK signal
if (!(USIS(EMSUART_UART) & (1 << UIBD))) {
// no bus collision - send terminating BRK signal
USC0(EMSUART_UART) |= (1 << UCLBE) | (1 << UCBRK); // enable loopback & set <BRK>
// wait until BRK detected...
while (!(USIR(EMSUART_UART) & (1 << UIBD))) {
delayMicroseconds(EMSUART_BIT_TIME);
}
USC0(EMSUART_UART) &= ~((1 << UCBRK) | (1 << UCLBE)); // disable loopback & clear <BRK>
USIC(EMSUART_UART) = (1 << UIBD); // clear BRK detect IRQ
phantomBreak = 1;
}
}
ETS_UART_INTR_ENABLE(); // open up the FIFO again to start receiving
return result; // send the Tx status back
}
/*
* flush everything left over in buffer, this clears both rx and tx FIFOs
*/
void ICACHE_FLASH_ATTR EMSuart::emsuart_flush_fifos() {
uint32_t tmp = ((1 << UCRXRST) | (1 << UCTXRST)); // bit mask
USC0(EMSUART_UART) |= (tmp); // set bits
USC0(EMSUART_UART) &= ~(tmp); // clear bits
}
/*
* Send a BRK signal
* Which is a 11-bit set of zero's (11 cycles)
*/
void ICACHE_FLASH_ATTR EMSuart::tx_brk() {
uint32_t tmp;
// must make sure Tx FIFO is empty
while (((USS(EMSUART_UART) >> USTXC) & 0xFF))
;
tmp = ((1 << UCRXRST) | (1 << UCTXRST)); // bit mask
USC0(EMSUART_UART) |= (tmp); // set bits
USC0(EMSUART_UART) &= ~(tmp); // clear bits
// To create a 11-bit <BRK> we set TXD_BRK bit so the break signal will
// automatically be sent when the tx fifo is empty
tmp = (1 << UCBRK);
USC0(EMSUART_UART) |= (tmp); // set bit
if (tx_mode_ == EMS_TXMODE_EMSPLUS) { // EMS+ mode
delayMicroseconds(EMSUART_TX_BRK_WAIT);
} else if (tx_mode_ == EMS_TXMODE_HT3) { // junkers mode
delayMicroseconds(EMSUART_TX_WAIT_BRK - EMSUART_TX_LAG); // 1144 (11 Bits)
}
USC0(EMSUART_UART) &= ~(tmp); // clear bit
}
} // namespace emsesp
#endif

View File

@@ -28,14 +28,26 @@
#define EMSUART_BAUD 9600 // uart baud rate for the EMS circuit
#define EMS_MAXBUFFERS 3 // buffers for circular filling to avoid collisions
#define EMS_MAXBUFFERSIZE 33 // max size of the buffer. EMS packets are max 32 bytes, plus extra 2 for BRK
#define EMS_MAXBUFFERSIZE 33 // max size of the buffer. EMS packets are max 32 bytes, plus extra 2 for BRKs
#define EMSUART_recvTaskPrio 1 // 0, 1 or 2. 0 being the lowest
#define EMSUART_recvTaskQueueLen 10 // number of queued'd Rx triggers
#define EMSUART_recvTaskQueueLen 10 // number of queued Rx triggers
#define EMS_TXMODE_DEFAULT 1
#define EMS_TXMODE_EMSPLUS 2
#define EMS_TXMODE_HT3 3
#define EMS_TXMODE_NEW 4 // for michael
// LEGACY
#define EMSUART_BIT_TIME 104 // bit time @9600 baud
#define EMSUART_TX_BRK_WAIT 2070 // the BRK from Boiler master is roughly 1.039ms, so accounting for hardware lag using around 2078 (for half-duplex) - 8 (lag)
#define EMSUART_TX_WAIT_BYTE (EMSUART_BIT_TIME * 10) // Time to send one Byte (8 Bits, 1 Start Bit, 1 Stop Bit)
#define EMSUART_TX_WAIT_BRK (EMSUART_BIT_TIME * 11) // Time to send a BRK Signal (11 Bit)
#define EMSUART_TX_WAIT_GAP (EMSUART_BIT_TIME * 7) // Gap between to Bytes
#define EMSUART_TX_LAG 8
#define EMSUART_BUSY_WAIT (EMSUART_BIT_TIME / 8)
#define EMS_TX_TO_CHARS (2 + 20)
#define EMS_TX_TO_COUNT (EMS_TX_TO_CHARS * 8)
namespace emsesp {
@@ -64,6 +76,9 @@ class EMSuart {
private:
static void ICACHE_RAM_ATTR emsuart_rx_intr_handler(void * para);
static void ICACHE_FLASH_ATTR emsuart_recvTask(os_event_t * events);
static void ICACHE_FLASH_ATTR emsuart_flush_fifos();
static void ICACHE_FLASH_ATTR tx_brk();
};
} // namespace emsesp

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "2.0.0a6"
#define EMSESP_APP_VERSION "2.0.0a8"