/* MQTT MODULE Copyright (C) 2016-2019 by Xose PĂ©rez Updated secure client support by Niek van der Maas < mail at niekvandermaas dot nl> */ #include "espurna.h" #if MQTT_SUPPORT #include #include #include "system.h" #include "mdns.h" #include "mqtt.h" #include "ntp.h" #include "rpc.h" #include "rtcmem.h" #include "ws.h" #include "libs/AsyncClientHelpers.h" #include "libs/SecureClientHelpers.h" #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT #include #include #elif MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT #include #elif MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT #include #endif // ----------------------------------------------------------------------------- namespace { #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT AsyncMqttClient _mqtt; #else // MQTT_LIBRARY_ARDUINOMQTT / MQTT_LIBRARY_PUBSUBCLIENT WiFiClient _mqtt_client; #if SECURE_CLIENT != SECURE_CLIENT_NONE std::unique_ptr _mqtt_client_secure = nullptr; #if MQTT_SECURE_CLIENT_INCLUDE_CA #include "static/mqtt_client_trusted_root_ca.h" // Assumes this header file defines a _mqtt_client_trusted_root_ca[] PROGMEM = "...PEM data..." #else #include "static/letsencrypt_isrgroot_pem.h" // Default to LetsEncrypt X3 certificate #define _mqtt_client_trusted_root_ca _ssl_letsencrypt_isrg_x3_ca #endif // MQTT_SECURE_CLIENT_INCLUDE_CA #endif // SECURE_CLIENT != SECURE_CLIENT_NONE #if MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT MQTTClient _mqtt(MQTT_BUFFER_MAX_SIZE); #elif MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT PubSubClient _mqtt; #endif #endif // MQTT_LIBRARY == MQTT_ASYNCMQTTCLIENT #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT struct MqttPidCallbackHandler { uint16_t pid; MqttPidCallback callback; }; using MqttPidCallbacks = std::forward_list; MqttPidCallbacks _mqtt_publish_callbacks; MqttPidCallbacks _mqtt_subscribe_callbacks; #endif std::forward_list _mqtt_heartbeat_callbacks; espurna::heartbeat::Mode _mqtt_heartbeat_mode; espurna::duration::Seconds _mqtt_heartbeat_interval; String _mqtt_payload_online; String _mqtt_payload_offline; std::forward_list _mqtt_callbacks; } // namespace // ----------------------------------------------------------------------------- // Settings // ----------------------------------------------------------------------------- namespace mqtt { using KeepAlive = std::chrono::duration; } // namespace mqtt namespace espurna { namespace settings { namespace internal { template<> mqtt::KeepAlive convert(const String& value) { return mqtt::KeepAlive { convert(value) }; } String serialize(mqtt::KeepAlive value) { return serialize(value.count()); } } // namespace internal } // namespace settings } // namespace espurna namespace mqtt { namespace build { namespace { static constexpr espurna::duration::Milliseconds SkipTime { MQTT_SKIP_TIME }; static constexpr espurna::duration::Milliseconds ReconnectDelayMin { MQTT_RECONNECT_DELAY_MIN }; static constexpr espurna::duration::Milliseconds ReconnectDelayMax { MQTT_RECONNECT_DELAY_MAX }; static constexpr espurna::duration::Milliseconds ReconnectStep { MQTT_RECONNECT_DELAY_STEP }; static constexpr size_t MessageLogMax { 128ul }; PROGMEM_STRING(Server, MQTT_SERVER); constexpr uint16_t port() { return MQTT_PORT; } constexpr bool enabled() { return 1 == MQTT_ENABLED; } constexpr bool autoconnect() { return 1 == MQTT_AUTOCONNECT; } PROGMEM_STRING(Topic, MQTT_TOPIC); PROGMEM_STRING(Getter, MQTT_GETTER); PROGMEM_STRING(Setter, MQTT_SETTER); PROGMEM_STRING(User, MQTT_USER); PROGMEM_STRING(Password, MQTT_PASS); constexpr int qos() { return MQTT_QOS; } constexpr bool retain() { return 1 == MQTT_RETAIN; } static constexpr KeepAlive KeepaliveMin { 15 }; static constexpr KeepAlive KeepaliveMax{ KeepAlive::max() }; constexpr KeepAlive keepalive() { return KeepAlive { MQTT_KEEPALIVE }; } static_assert(keepalive() >= KeepaliveMin, ""); static_assert(keepalive() <= KeepaliveMax, ""); PROGMEM_STRING(TopicWill, MQTT_TOPIC_STATUS); constexpr bool json() { return 1 == MQTT_USE_JSON; } static constexpr auto JsonDelay = espurna::duration::Milliseconds(MQTT_USE_JSON_DELAY); PROGMEM_STRING(TopicJson, MQTT_TOPIC_JSON); constexpr espurna::duration::Milliseconds skipTime() { return espurna::duration::Milliseconds(MQTT_SKIP_TIME); } PROGMEM_STRING(PayloadOnline, MQTT_STATUS_ONLINE); PROGMEM_STRING(PayloadOffline, MQTT_STATUS_OFFLINE); constexpr bool secure() { return 1 == MQTT_SSL_ENABLED; } int secureClientCheck() { return MQTT_SECURE_CLIENT_CHECK; } PROGMEM_STRING(Fingerprint, MQTT_SSL_FINGERPRINT); constexpr uint16_t mfln() { return MQTT_SECURE_CLIENT_MFLN; } } // namespace } // namespace build namespace settings { namespace keys { namespace { PROGMEM_STRING(Server, "mqttServer"); PROGMEM_STRING(Port, "mqttPort"); PROGMEM_STRING(Enabled, "mqttEnabled"); PROGMEM_STRING(Autoconnect, "mqttAutoconnect"); PROGMEM_STRING(Topic, "mqttTopic"); PROGMEM_STRING(Getter, "mqttGetter"); PROGMEM_STRING(Setter, "mqttSetter"); PROGMEM_STRING(User, "mqttUser"); PROGMEM_STRING(Password, "mqttPassword"); PROGMEM_STRING(QoS, "mqttQoS"); PROGMEM_STRING(Retain, "mqttRetain"); PROGMEM_STRING(Keepalive, "mqttKeep"); PROGMEM_STRING(ClientId, "mqttClientID"); PROGMEM_STRING(TopicWill, "mqttWill"); PROGMEM_STRING(UseJson, "mqttUseJson"); PROGMEM_STRING(TopicJson, "mqttJson"); PROGMEM_STRING(HeartbeatMode, "mqttHbMode"); PROGMEM_STRING(HeartbeatInterval, "mqttHbIntvl"); PROGMEM_STRING(SkipTime, "mqttSkipTime"); PROGMEM_STRING(PayloadOnline, "mqttPayloadOnline"); PROGMEM_STRING(PayloadOffline, "mqttPayloadOffline"); PROGMEM_STRING(Secure, "mqttUseSSL"); PROGMEM_STRING(Fingerprint, "mqttFP"); PROGMEM_STRING(SecureClientCheck, "mqttScCheck"); PROGMEM_STRING(SecureClientMfln, "mqttScMFLN"); } // namespace } // namespace keys namespace { String server() { return getSetting(keys::Server, espurna::StringView(build::Server)); } uint16_t port() { return getSetting(keys::Port, build::port()); } bool enabled() { return getSetting(keys::Enabled, build::enabled()); } bool autoconnect() { return getSetting(keys::Autoconnect, build::autoconnect()); } String topic() { return getSetting(keys::Topic, espurna::StringView(build::Topic)); } String getter() { return getSetting(keys::Getter, espurna::StringView(build::Getter)); } String setter() { return getSetting(keys::Setter, espurna::StringView(build::Setter)); } String user() { return getSetting(keys::User, espurna::StringView(build::User)); } String password() { return getSetting(keys::Password, espurna::StringView(build::Password)); } int qos() { return getSetting(keys::QoS, build::qos()); } bool retain() { return getSetting(keys::Retain, build::retain()); } KeepAlive keepalive() { return std::clamp( getSetting(keys::Keepalive, build::keepalive()), build::KeepaliveMin, build::KeepaliveMax); } String clientId() { return getSetting(keys::ClientId, systemIdentifier()); } String topicWill() { return getSetting(keys::TopicWill, espurna::StringView(build::TopicWill)); } bool json() { return getSetting(keys::UseJson, build::json()); } String topicJson() { return getSetting(keys::TopicJson, espurna::StringView(build::TopicJson)); } espurna::heartbeat::Mode heartbeatMode() { return getSetting(keys::HeartbeatMode, espurna::heartbeat::currentMode()); } espurna::duration::Seconds heartbeatInterval() { return getSetting(keys::HeartbeatInterval, espurna::heartbeat::currentInterval()); } espurna::duration::Milliseconds skipTime() { return getSetting(keys::SkipTime, build::skipTime()); } String payloadOnline() { return getSetting(keys::PayloadOnline, espurna::StringView(build::PayloadOnline)); } String payloadOffline() { return getSetting(keys::PayloadOffline, espurna::StringView(build::PayloadOffline)); } [[gnu::unused]] bool secure() { return getSetting(keys::Secure, build::secure()); } [[gnu::unused]] int secureClientCheck() { return getSetting(keys::SecureClientCheck, build::secureClientCheck()); } [[gnu::unused]] String fingerprint() { return getSetting(keys::Fingerprint, espurna::StringView(build::Fingerprint)); } [[gnu::unused]] uint16_t mfln() { return getSetting(keys::SecureClientMfln, build::mfln()); } } // namespace namespace query { namespace { namespace internal { #define EXACT_VALUE(NAME, FUNC)\ String NAME () {\ return espurna::settings::internal::serialize(FUNC());\ } EXACT_VALUE(port, settings::port) EXACT_VALUE(enabled, settings::enabled) EXACT_VALUE(autoconnect, settings::autoconnect) EXACT_VALUE(qos, settings::qos) EXACT_VALUE(retain, settings::retain) EXACT_VALUE(keepalive, settings::keepalive) EXACT_VALUE(json, settings::json) EXACT_VALUE(heartbeatMode, settings::heartbeatMode) EXACT_VALUE(heartbeatInterval, settings::heartbeatInterval) EXACT_VALUE(skipTime, settings::skipTime) #undef EXACT_VALUE } // namespace internal static constexpr espurna::settings::query::Setting Settings[] PROGMEM { {keys::Server, settings::server}, {keys::Port, internal::port}, {keys::Enabled, internal::enabled}, {keys::Autoconnect, internal::autoconnect}, {keys::Topic, settings::topic}, {keys::Getter, settings::getter}, {keys::Setter, settings::setter}, {keys::User, settings::user}, {keys::Password, settings::password}, {keys::QoS, internal::qos}, {keys::Retain, internal::retain}, {keys::Keepalive, internal::keepalive}, {keys::ClientId, settings::clientId}, {keys::TopicWill, settings::topicWill}, {keys::UseJson, internal::json}, {keys::TopicJson, settings::topicJson}, {keys::HeartbeatMode, internal::heartbeatMode}, {keys::HeartbeatInterval, internal::heartbeatInterval}, {keys::SkipTime, internal::skipTime}, {keys::PayloadOnline, settings::payloadOnline}, {keys::PayloadOffline, settings::payloadOffline}, }; bool checkSamePrefix(espurna::StringView key) { return espurna::settings::query::samePrefix(key, STRING_VIEW("mqtt")); } String findValueFrom(espurna::StringView key) { return espurna::settings::query::Setting::findValueFrom(Settings, key); } void setup() { ::settingsRegisterQueryHandler({ .check = checkSamePrefix, .get = findValueFrom }); } } // namespace } // namespace query } // namespace settings } // namespace mqtt namespace { using MqttTimeSource = espurna::time::CoreClock; MqttTimeSource::time_point _mqtt_last_connection{}; MqttTimeSource::duration _mqtt_skip_time { mqtt::build::SkipTime }; MqttTimeSource::duration _mqtt_reconnect_delay { mqtt::build::ReconnectDelayMin }; AsyncClientState _mqtt_state { AsyncClientState::Disconnected }; bool _mqtt_skip_messages { false }; bool _mqtt_enabled { mqtt::build::enabled() }; bool _mqtt_use_json { mqtt::build::json() }; bool _mqtt_forward { false }; struct MqttConnectionSettings { bool retain { mqtt::build::retain() }; int qos { mqtt::build::qos() }; mqtt::KeepAlive keepalive { mqtt::build::keepalive() }; String topic; String topic_json; String setter; String getter; String user; String pass; String will; String server; uint16_t port { 0 }; String clientId; }; static MqttConnectionSettings _mqtt_settings; template static void _mqttApplySetting(Lhs& lhs, Rhs&& rhs) { if (lhs != rhs) { lhs = std::forward(rhs); mqttDisconnect(); } } // Can't have **any** MQTT placeholders but our own `{magnitude}` bool _mqttValidTopicString(espurna::StringView value) { size_t hash = 0; size_t plus = 0; for (auto it = value.begin(); it != value.end(); ++it) { switch (*it) { case '#': ++hash; break; case '+': ++plus; break; } } return (hash <= 1) && (plus == 0); } bool _mqttApplyValidTopicString(String& lhs, String&& rhs) { if (_mqttValidTopicString(rhs)) { _mqttApplySetting(lhs, std::move(rhs)); return true; } mqttDisconnect(); return false; } } // namespace // ----------------------------------------------------------------------------- // JSON payload // ----------------------------------------------------------------------------- namespace { struct MqttPayload { MqttPayload() = delete; MqttPayload(const MqttPayload&) = default; // TODO: replace String implementation with Core v3 (or just use newer Core) // 2.7.x still has basic Arduino String move ctor that is not noexcept MqttPayload(MqttPayload&& other) noexcept : _topic(std::move(other._topic)), _message(std::move(other._message)) {} template MqttPayload(Topic&& topic, Message&& message) : _topic(std::forward(topic)), _message(std::forward(message)) {} const String& topic() const { return _topic; } const String& message() const { return _message; } private: String _topic; String _message; }; size_t _mqtt_json_payload_count { 0ul }; std::forward_list _mqtt_json_payload; espurna::timer::SystemTimer _mqtt_json_payload_flush; } // namespace // ----------------------------------------------------------------------------- // Secure client handlers // ----------------------------------------------------------------------------- namespace { #if SECURE_CLIENT != SECURE_CLIENT_NONE SecureClientConfig _mqtt_sc_config { .tag = "MQTT", #if SECURE_CLIENT == SECURE_CLIENT_AXTLS .on_host = []() -> String { return _mqtt_server; }, #endif .on_check = mqtt::settings::secureClientCheck, #if SECURE_CLIENT == SECURE_CLIENT_BEARSSL .on_certificate = []() -> const char* { return _mqtt_client_trusted_root_ca; }, #endif .on_fingerprint = mqtt::settings::fingerprint, #if SECURE_CLIENT == SECURE_CLIENT_BEARSSL .on_mfln = mqtt::settings::mfln, #endif .debug = true, }; #endif } // namespace // ----------------------------------------------------------------------------- // Client configuration & setup // ----------------------------------------------------------------------------- namespace { // TODO: MQTT standard has some weird rules about session persistance on the broker // ref. 3.1.2.4 Clean Session, where we are uniquely identified by the client-id: // - subscriptions that are no longer useful are still there // unsub # will be acked, but we were never subbed to # to begin with ... // - we *will* receive messages that were sent using qos 1 or 2 while we were offline // which is only sort-of good, but MQTT broker v3 will never timeout those messages. // this would be the main reason for turning ON the clean session // - connecting with clean session ON will purge existing session *and* also prevent // the broker from caching the messages after the current connection ends. // there is no middle-ground, where previous session is removed but the current one is preserved // so, turning it ON <-> OFF during runtime is not very useful :/ // // Pending MQTT v5 client #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT void _mqttSetupAsyncClient(bool secure = false) { _mqtt.setServer(_mqtt_settings.server.c_str(), _mqtt_settings.port); _mqtt.setClientId(_mqtt_settings.clientId.c_str()); _mqtt.setKeepAlive(_mqtt_settings.keepalive.count()); _mqtt.setCleanSession(false); _mqtt.setWill( _mqtt_settings.will.c_str(), _mqtt_settings.qos, _mqtt_settings.retain, _mqtt_payload_offline.c_str()); if (_mqtt_settings.user.length() && _mqtt_settings.pass.length()) { DEBUG_MSG_P(PSTR("[MQTT] Connecting as user %s\n"), _mqtt_settings.user.c_str()); _mqtt.setCredentials( _mqtt_settings.user.c_str(), _mqtt_settings.pass.c_str()); } #if SECURE_CLIENT != SECURE_CLIENT_NONE if (secure) { DEBUG_MSG_P(PSTR("[MQTT] Using SSL\n")); _mqtt.setSecure(secure); } #endif // SECURE_CLIENT != SECURE_CLIENT_NONE _mqtt.connect(); } #endif // MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT #if (MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT) || (MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT) WiFiClient& _mqttGetClient(bool secure) { #if SECURE_CLIENT != SECURE_CLIENT_NONE return (secure ? _mqtt_client_secure->get() : _mqtt_client); #else return _mqtt_client; #endif } bool _mqttSetupSyncClient(bool secure = false) { #if SECURE_CLIENT != SECURE_CLIENT_NONE if (secure) { if (!_mqtt_client_secure) _mqtt_client_secure = std::make_unique(_mqtt_sc_config); return _mqtt_client_secure->beforeConnected(); } #endif return true; } bool _mqttConnectSyncClient(bool secure = false) { bool result = false; #if MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT _mqtt.begin(_mqtt_settings.server.c_str(), _mqtt_settings.port, _mqttGetClient(secure)); _mqtt.setWill(_mqtt_settings.will.c_str(), _mqtt_payload_offline.c_str(), _mqtt_settings.retain, _mqtt_settings.qos); _mqtt.setKeepAlive(_mqtt_settings.keepalive.count()); result = _mqtt.connect( _mqtt_settings.clientId.c_str(), _mqtt_settings.user.c_str(), _mqtt_settings.pass.c_str()); #elif MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT _mqtt.setClient(_mqttGetClient(secure)); _mqtt.setServer(_mqtt_settings.server.c_str(), _mqtt_port); if (_mqtt_settings.user.length() && _mqtt_settings.pass.length()) { DEBUG_MSG_P(PSTR("[MQTT] Connecting as user %s\n"), _mqtt_settings.user.c_str()); result = _mqtt.connect( _mqtt_settings.clientid.c_str(), _mqtt_settings.user.c_str(), _mqtt_settings.pass.c_str(), _mqtt_settings.will.c_str(), _mqtt_settings.qos, _mqtt_settings.retain, _mqtt_payload_offline.c_str()); } else { result = _mqtt.connect( _mqtt_settings.clientid.c_str(), _mqtt_settings.will.c_str(), _mqtt_settings.qos, _mqtt_settings.retain, _mqtt_payload_offline.c_str()); } #endif #if SECURE_CLIENT != SECURE_CLIENT_NONE if (result && secure) { result = _mqtt_client_secure->afterConnected(); } #endif return result; } #endif // (MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT) || (MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT) String _mqttPlaceholders(String text) { static const String mac = String(systemChipId()); text.replace(F("{mac}"), mac); text.replace(F("{hostname}"), systemHostname()); text.replace(F("{magnitude}"), F("#")); return text; } #if MDNS_SERVER_SUPPORT void _mqttMdnsSchedule(); void _mqttMdnsStop(); #endif void _mqttConfigure() { // Make sure we have both the server to connect to things are enabled { _mqttApplySetting(_mqtt_settings.server, mqtt::settings::server()); _mqttApplySetting(_mqtt_settings.port, mqtt::settings::port()); _mqttApplySetting(_mqtt_enabled, mqtt::settings::enabled()); #if MDNS_SERVER_SUPPORT if (!_mqtt_enabled) { _mqttMdnsStop(); } #endif if (!_mqtt_settings.server.length()) { #if MDNS_SERVER_SUPPORT // But, start mdns discovery when it would've been enabled if (_mqtt_enabled && mqtt::settings::autoconnect()) { _mqttMdnsSchedule(); } #endif return; } } // Get base topic and apply placeholders { // Replace things inside curly braces (like {hostname}, {mac} etc.) auto topic = _mqttPlaceholders(mqtt::settings::topic()); if (!_mqttValidTopicString(topic)) { mqttDisconnect(); return; } // Topic **must** end with some kind of word if (topic.endsWith("/")) { topic.remove(topic.length() - 1); } // For simple topics, sssume right-hand side contains magnitude if (topic.indexOf("#") == -1) { topic.concat("/#"); } _mqttApplySetting(_mqtt_settings.topic, std::move(topic)); } // Getter and setter _mqttApplyValidTopicString(_mqtt_settings.getter, mqtt::settings::getter()); _mqttApplyValidTopicString(_mqtt_settings.setter, mqtt::settings::setter()); _mqttApplySetting(_mqtt_forward, !_mqtt_settings.setter.equals(_mqtt_settings.getter)); // Last will aka status topic // (note that *must* be after topic updates) _mqttApplyValidTopicString(_mqtt_settings.will, mqttTopic(mqtt::settings::topicWill())); // MQTT options _mqttApplySetting(_mqtt_settings.user, _mqttPlaceholders(mqtt::settings::user())); _mqttApplySetting(_mqtt_settings.pass, mqtt::settings::password()); _mqttApplySetting(_mqtt_settings.clientId, _mqttPlaceholders(mqtt::settings::clientId())); _mqttApplySetting(_mqtt_settings.qos, mqtt::settings::qos()); _mqttApplySetting(_mqtt_settings.retain, mqtt::settings::retain()); _mqttApplySetting(_mqtt_settings.keepalive, mqtt::settings::keepalive()); // MQTT JSON _mqttApplySetting(_mqtt_use_json, mqtt::settings::json()); _mqttApplyValidTopicString(_mqtt_settings.topic_json, mqttTopic(mqtt::settings::topicJson())); // Heartbeat messages _mqttApplySetting(_mqtt_heartbeat_mode, mqtt::settings::heartbeatMode()); _mqttApplySetting(_mqtt_heartbeat_interval, mqtt::settings::heartbeatInterval()); _mqtt_skip_time = mqtt::settings::skipTime(); // Custom payload strings _mqtt_payload_online = mqtt::settings::payloadOnline(); _mqtt_payload_offline = mqtt::settings::payloadOffline(); // Reset reconnect delay to reconnect sooner _mqtt_reconnect_delay = mqtt::build::ReconnectDelayMin; } #if MDNS_SERVER_SUPPORT constexpr auto MqttMdnsDiscoveryInterval = espurna::duration::Seconds(15); espurna::timer::SystemTimer _mqtt_mdns_discovery; void _mqttMdnsStop() { _mqtt_mdns_discovery.stop(); } void _mqttMdnsDiscovery(); void _mqttMdnsSchedule() { _mqtt_mdns_discovery.once(MqttMdnsDiscoveryInterval, _mqttMdnsDiscovery); } void _mqttMdnsDiscovery() { if (mdnsRunning()) { DEBUG_MSG_P(PSTR("[MQTT] Querying MDNS service _mqtt._tcp\n")); auto found = mdnsServiceQuery("mqtt", "tcp", [](String&& server, uint16_t port) { DEBUG_MSG_P(PSTR("[MQTT] MDNS found broker at %s:%hu\n"), server.c_str(), port); setSetting("mqttServer", server); setSetting("mqttPort", port); return true; }); if (found) { _mqttMdnsStop(); _mqttConfigure(); return; } } _mqttMdnsSchedule(); } #endif void _mqttBackwards() { auto topic = mqtt::settings::topic(); if (topic.indexOf("{identifier}") > 0) { topic.replace("{identifier}", "{hostname}"); setSetting("mqttTopic", topic); } } #define __MQTT_INFO_STR(X) #X #define _MQTT_INFO_STR(X) __MQTT_INFO_STR(X) alignas(4) static constexpr char MqttBuild[] PROGMEM_STRING_ATTR { #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT "AsyncMqttClient" #elif MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT "Arduino-MQTT" #elif MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT "PubSubClient" #endif #if SECURE_CLIENT != SEURE_CLIENT_NONE " (w/ SECURE CLIENT)" #endif " Buffer size " _MQTT_INFO_STR(MQTT_BUFFER_MAX_SIZE) " (bytes)" }; #undef _MQTT_INFO_STR #undef __MQTT_INFO_STR constexpr espurna::StringView _mqttBuildInfo() { return MqttBuild; } String _mqttClientState(AsyncClientState state) { espurna::StringView out; switch (state) { case AsyncClientState::Connecting: out = STRING_VIEW("CONNECTING"); break; case AsyncClientState::Connected: out = STRING_VIEW("CONNECTED"); break; case AsyncClientState::Disconnected: out = STRING_VIEW("DISCONNECTED"); break; case AsyncClientState::Disconnecting: out = STRING_VIEW("DISCONNECTING"); break; default: out = STRING_VIEW("WAITING"); break; } return out.toString(); } String _mqttClientInfo(bool enabled, AsyncClientState state) { String out; if (_mqtt_enabled) { out += _mqttClientState(state); } else { out += STRING_VIEW("DISABLED"); } return out; } String _mqttClientInfo() { return _mqttClientInfo(_mqtt_enabled, _mqtt_state); } void _mqttInfo() { constexpr auto build = _mqttBuildInfo(); DEBUG_MSG_P(PSTR("[MQTT] %.*s\n"), build.length(), build.data()); const auto client = _mqttClientInfo(); DEBUG_MSG_P(PSTR("[MQTT] Client %.*s\n"), client.length(), client.c_str()); if (_mqtt_enabled && (_mqtt_state != AsyncClientState::Connected)) { DEBUG_MSG_P(PSTR("[MQTT] Retrying, Last %u with Delay %u (Step %u)\n"), _mqtt_last_connection.time_since_epoch().count(), _mqtt_reconnect_delay.count(), mqtt::build::ReconnectStep.count()); } } } // namespace // ----------------------------------------------------------------------------- // WEB // ----------------------------------------------------------------------------- namespace { #if WEB_SUPPORT bool _mqttWebSocketOnKeyCheck(espurna::StringView key, const JsonVariant&) { return mqtt::settings::query::checkSamePrefix(key); } void _mqttWebSocketOnVisible(JsonObject& root) { wsPayloadModule(root, PSTR("mqtt")); #if SECURE_CLIENT != SECURE_CLIENT_NONE wsPayloadModule(root, PSTR("mqttssl")); #endif } void _mqttWebSocketOnData(JsonObject& root) { root[F("mqttStatus")] = mqttConnected(); } void _mqttWebSocketOnConnected(JsonObject& root) { using namespace mqtt::settings::keys; using mqtt::settings::keys::Server; root[FPSTR(Enabled)] = mqttEnabled(); root[FPSTR(Server)] = mqtt::settings::server(); root[FPSTR(Port)] = mqtt::settings::port(); root[FPSTR(User)] = mqtt::settings::user(); root[FPSTR(Password)] = mqtt::settings::password(); root[FPSTR(Retain)] = mqtt::settings::retain(); root[FPSTR(Keepalive)] = mqtt::settings::keepalive().count(); root[FPSTR(ClientId)] = mqtt::settings::clientId(); root[FPSTR(QoS)] = mqtt::settings::qos(); #if SECURE_CLIENT != SECURE_CLIENT_NONE root[FPSTR(Secure)] = mqtt::settings::secure(); root[FPSTR(Fingerprint)] = mqtt::settings::fingerprint(); #endif root[FPSTR(Topic)] = mqtt::settings::topic(); root[FPSTR(UseJson)] = mqtt::settings::json(); } #endif } // namespace // ----------------------------------------------------------------------------- // SETTINGS // ----------------------------------------------------------------------------- #if TERMINAL_SUPPORT namespace { PROGMEM_STRING(MqttCommand, "MQTT"); static void _mqttCommand(::terminal::CommandContext&& ctx) { constexpr auto build = _mqttBuildInfo(); ctx.output.printf_P(PSTR("%.*s\n"), build.length(), build.c_str()); const auto client = _mqttClientInfo(); ctx.output.printf_P(PSTR("client %.*s\n"), client.length(), client.c_str()); settingsDump(ctx, mqtt::settings::query::Settings); terminalOK(ctx); } PROGMEM_STRING(MqttCommandReset, "MQTT.RESET"); static void _mqttCommandReset(::terminal::CommandContext&& ctx) { _mqttConfigure(); mqttDisconnect(); terminalOK(ctx); } PROGMEM_STRING(MqttCommandSend, "MQTT.SEND"); static void _mqttCommandSend(::terminal::CommandContext&& ctx) { if (ctx.argv.size() == 3) { if (mqttSend(ctx.argv[1].c_str(), ctx.argv[2].c_str(), false, false)) { terminalOK(ctx); } else { terminalError(ctx, F("Cannot queue the message")); } return; } terminalError(ctx, F("MQTT.SEND ")); } static constexpr ::terminal::Command MqttCommands[] PROGMEM { {MqttCommand, _mqttCommand}, {MqttCommandReset, _mqttCommandReset}, {MqttCommandSend, _mqttCommandSend}, }; void _mqttCommandsSetup() { espurna::terminal::add(MqttCommands); } } // namespace #endif // TERMINAL_SUPPORT // ----------------------------------------------------------------------------- // MQTT Callbacks // ----------------------------------------------------------------------------- namespace { void _mqttCallback(unsigned int type, espurna::StringView topic, espurna::StringView payload) { if (type == MQTT_CONNECT_EVENT) { mqttSubscribe(MQTT_TOPIC_ACTION); } if (type == MQTT_MESSAGE_EVENT) { auto t = mqttMagnitude(topic); if (t.equals(MQTT_TOPIC_ACTION)) { rpcHandleAction(payload); } } } bool _mqttHeartbeat(espurna::heartbeat::Mask mask) { // No point retrying, since we will be re-scheduled on connection if (!mqttConnected()) { return true; } #if NTP_SUPPORT // Backported from the older utils implementation. // Wait until the time is synced to avoid sending partial report *and* // as a result, wait until the next interval to actually send the datetime string. if ((mask & espurna::heartbeat::Report::Datetime) && !ntpSynced()) { return false; } #endif // TODO: rework old HEARTBEAT_REPEAT_STATUS? // for example: send full report once, send only the dynamic data after that // (interval, hostname, description, ssid, bssid, ip, mac, rssi, uptime, datetime, heap, loadavg, vcc) // otherwise, it is still possible by setting everything to 0 *but* the Report::Status bit // TODO: per-module mask? // TODO: simply send static data with onConnected, and the rest from here? if (mask & espurna::heartbeat::Report::Status) mqttSendStatus(); if (mask & espurna::heartbeat::Report::Interval) mqttSend(MQTT_TOPIC_INTERVAL, String(_mqtt_heartbeat_interval.count()).c_str()); const auto app = buildApp(); if (mask & espurna::heartbeat::Report::App) mqttSend(MQTT_TOPIC_APP, String(app.name).c_str()); if (mask & espurna::heartbeat::Report::Version) mqttSend(MQTT_TOPIC_VERSION, String(app.version).c_str()); if (mask & espurna::heartbeat::Report::Board) mqttSend(MQTT_TOPIC_BOARD, systemDevice().c_str()); if (mask & espurna::heartbeat::Report::Hostname) mqttSend(MQTT_TOPIC_HOSTNAME, systemHostname().c_str()); if (mask & espurna::heartbeat::Report::Description) { const auto value = systemDescription(); if (value.length()) { mqttSend(MQTT_TOPIC_DESCRIPTION, value.c_str()); } } if (mask & espurna::heartbeat::Report::Ssid) mqttSend(MQTT_TOPIC_SSID, WiFi.SSID().c_str()); if (mask & espurna::heartbeat::Report::Bssid) mqttSend(MQTT_TOPIC_BSSID, WiFi.BSSIDstr().c_str()); if (mask & espurna::heartbeat::Report::Ip) mqttSend(MQTT_TOPIC_IP, wifiStaIp().toString().c_str()); if (mask & espurna::heartbeat::Report::Mac) mqttSend(MQTT_TOPIC_MAC, WiFi.macAddress().c_str()); if (mask & espurna::heartbeat::Report::Rssi) mqttSend(MQTT_TOPIC_RSSI, String(WiFi.RSSI()).c_str()); if (mask & espurna::heartbeat::Report::Uptime) mqttSend(MQTT_TOPIC_UPTIME, String(systemUptime().count()).c_str()); #if NTP_SUPPORT if (mask & espurna::heartbeat::Report::Datetime) mqttSend(MQTT_TOPIC_DATETIME, ntpDateTime().c_str()); #endif if (mask & espurna::heartbeat::Report::Freeheap) { const auto stats = systemHeapStats(); mqttSend(MQTT_TOPIC_FREEHEAP, String(stats.available).c_str()); } if (mask & espurna::heartbeat::Report::Loadavg) mqttSend(MQTT_TOPIC_LOADAVG, String(systemLoadAverage()).c_str()); if ((mask & espurna::heartbeat::Report::Vcc) && (ADC_MODE_VALUE == ADC_VCC)) mqttSend(MQTT_TOPIC_VCC, String(ESP.getVcc()).c_str()); auto status = mqttConnected(); for (auto& cb : _mqtt_heartbeat_callbacks) { status = status && cb(mask); } return status; } void _mqttOnConnect() { _mqtt_reconnect_delay = mqtt::build::ReconnectDelayMin; _mqtt_last_connection = MqttTimeSource::now(); _mqtt_state = AsyncClientState::Connected; systemHeartbeat(_mqttHeartbeat, _mqtt_heartbeat_mode, _mqtt_heartbeat_interval); // Notify all subscribers about the connection for (const auto callback : _mqtt_callbacks) { callback(MQTT_CONNECT_EVENT, espurna::StringView(), espurna::StringView()); } DEBUG_MSG_P(PSTR("[MQTT] Connected!\n")); } void _mqttOnDisconnect() { #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT _mqtt_publish_callbacks.clear(); _mqtt_subscribe_callbacks.clear(); #endif _mqtt_last_connection = MqttTimeSource::now(); _mqtt_state = AsyncClientState::Disconnected; systemStopHeartbeat(_mqttHeartbeat); // Notify all subscribers about the disconnect for (const auto callback : _mqtt_callbacks) { callback(MQTT_DISCONNECT_EVENT, espurna::StringView(), espurna::StringView()); } DEBUG_MSG_P(PSTR("[MQTT] Disconnected!\n")); } #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT // Run the associated callback when message PID is acknowledged by the broker void _mqttPidCallback(MqttPidCallbacks& callbacks, uint16_t pid) { if (callbacks.empty()) { return; } auto end = callbacks.end(); auto prev = callbacks.before_begin(); auto it = callbacks.begin(); while (it != end) { if ((*it).pid == pid) { (*it).callback(); it = callbacks.erase_after(prev); } else { prev = it; ++it; } } } #endif // Force-skip everything received in a short window right after connecting to avoid syncronization issues. bool _mqttMaybeSkipRetained(char* topic) { if (_mqtt_skip_messages && (MqttTimeSource::now() - _mqtt_last_connection < _mqtt_skip_time)) { DEBUG_MSG_P(PSTR("[MQTT] Received %s - SKIPPED\n"), topic); return true; } _mqtt_skip_messages = false; return false; } #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT // MQTT Broker can sometimes send messages in bulk. Even when message size is less than MQTT_BUFFER_MAX_SIZE, we *could* // receive a message with `len != total`, this requiring buffering of the received data. Prepare a static memory to store the // data until `(len + index) == total`. // TODO: One pending issue is streaming arbitrary data (e.g. binary, for OTA). We always set '\0' and API consumer expects C-String. // In that case, there could be MQTT_MESSAGE_RAW_EVENT and this callback only trigger on small messages. // TODO: Current callback model does not allow to pass message length. Instead, implement a topic filter and record all subscriptions. That way we don't need to filter out events and could implement per-event callbacks. void _mqttOnMessageAsync(char* topic, char* payload, AsyncMqttClientMessageProperties, size_t len, size_t index, size_t total) { static constexpr size_t BufferSize { MQTT_BUFFER_MAX_SIZE }; static_assert(BufferSize > 0, ""); if (!len || (len > BufferSize) || (total > BufferSize)) { return; } if (_mqttMaybeSkipRetained(topic)) { return; } alignas(4) static char buffer[((BufferSize + 3) & ~3) + 4] = {0}; std::copy(payload, payload + len, buffer); // Not done yet if (total != (len + index)) { DEBUG_MSG_P(PSTR("[MQTT] Buffered %s => %u / %u bytes\n"), topic, len, total); return; } buffer[len + index] = '\0'; if (len < mqtt::build::MessageLogMax) { DEBUG_MSG_P(PSTR("[MQTT] Received %s => %s\n"), topic, buffer); } else { DEBUG_MSG_P(PSTR("[MQTT] Received %s => (%u bytes)\n"), topic, len); } auto topic_view = espurna::StringView{ topic }; auto message_view = espurna::StringView{ &buffer[0], &buffer[total] }; for (const auto callback : _mqtt_callbacks) { callback(MQTT_MESSAGE_EVENT, topic_view, message_view); } } #else // Sync client already implements buffering, but we still need to add '\0' because API consumer expects C-String :/ // TODO: consider reworking this (and async counterpart), giving callback func length of the message. void _mqttOnMessage(char* topic, char* payload, unsigned int len) { if (!len || (len > MQTT_BUFFER_MAX_SIZE)) return; if (_mqttMaybeSkipRetained(topic)) return; static char message[((MQTT_BUFFER_MAX_SIZE + 1) + 31) & -32] = {0}; memmove(message, (char *) payload, len); message[len] = '\0'; DEBUG_MSG_P(PSTR("[MQTT] Received %s => %s\n"), topic, message); // Call subscribers with the message buffer for (auto& callback : _mqtt_callbacks) { callback(MQTT_MESSAGE_EVENT, topic, message); } } #endif // MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT } // namespace // ----------------------------------------------------------------------------- // Public API // ----------------------------------------------------------------------------- // Return {magnitude} (aka #) part of the topic string // e.g. // * /#/set - generic topic placement // ^ // * /#//set - when {magnitude} is used // ^ // * #//set - when magnitude is at the start // ^ // * #/set - when *only* {magnitude} is used (or, empty topic string) // ^ // Depends on the topic and setter settings values. // Note that function is ignoring the fact that these strings may not contain the // root topic b/c MQTT handles that instead of us (and it's good idea to trust it). espurna::StringView mqttMagnitude(espurna::StringView topic) { using espurna::StringView; StringView out; const auto pattern = _mqtt_settings.topic + _mqtt_settings.setter; auto it = std::find(pattern.begin(), pattern.end(), '#'); if (it == pattern.end()) { return out; } const auto start = StringView(pattern.begin(), it); if (start.length()) { topic = StringView(topic.begin() + start.length(), topic.end()); } const auto end = StringView(it + 1, pattern.end()); if (end.length()) { topic = StringView(topic.begin(), topic.end() - end.length()); } out = StringView(topic.begin(), topic.end()); return out; } // Creates a proper MQTT topic for on the given 'magnitude' static String _mqttTopicWith(String magnitude) { String out; out.reserve(magnitude.length() + _mqtt_settings.topic.length() + _mqtt_settings.setter.length() + _mqtt_settings.getter.length()); out += _mqtt_settings.topic; out.replace("#", magnitude); return out; } // When magnitude is a status topic aka getter static String _mqttTopicGetter(String magnitude) { return _mqttTopicWith(magnitude) + _mqtt_settings.getter; } // When magnitude is an input topic aka setter String _mqttTopicSetter(String magnitude) { return _mqttTopicWith(magnitude) + _mqtt_settings.setter; } // When magnitude is indexed, append its index to the topic static String _mqttTopicIndexed(String topic, size_t index) { return topic + '/' + String(index, 10); } String mqttTopic(const String& magnitude) { return _mqttTopicGetter(magnitude); } String mqttTopic(const String& magnitude, size_t index) { return _mqttTopicGetter(_mqttTopicIndexed(magnitude, index)); } String mqttTopicSetter(const String& magnitude) { return _mqttTopicSetter(magnitude); } String mqttTopicSetter(const String& magnitude, size_t index) { return _mqttTopicSetter(_mqttTopicIndexed(magnitude, index)); } // ----------------------------------------------------------------------------- uint16_t mqttSendRaw(const char* topic, const char* message, bool retain, int qos) { if (_mqtt.connected()) { const unsigned int packetId { #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT _mqtt.publish(topic, qos, retain, message) #elif MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT _mqtt.publish(topic, message, retain, qos) #elif MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT _mqtt.publish(topic, message, retain) #endif }; #if DEBUG_SUPPORT { const size_t len = strlen(message); auto begin = message; auto end = message + len; if ((len > mqtt::build::MessageLogMax) || (end != std::find(begin, end, '\n'))) { DEBUG_MSG_P(PSTR("[MQTT] Sending %s => (%u bytes) (PID %u)\n"), topic, len, packetId); } else { DEBUG_MSG_P(PSTR("[MQTT] Sending %s => %s (PID %u)\n"), topic, message, packetId); } } #endif return packetId; } return false; } uint16_t mqttSendRaw(const char* topic, const char* message, bool retain) { return mqttSendRaw(topic, message, retain, _mqtt_settings.qos); } uint16_t mqttSendRaw(const char* topic, const char* message) { return mqttSendRaw(topic, message, _mqtt_settings.retain); } bool mqttSend(const char* topic, const char* message, bool force, bool retain) { if (!force && _mqtt_use_json) { mqttEnqueue(topic, message); _mqtt_json_payload_flush.once(mqtt::build::JsonDelay, mqttFlush); return true; } return mqttSendRaw(mqttTopic(topic).c_str(), message, retain) > 0; } bool mqttSend(const char* topic, const char* message, bool force) { return mqttSend(topic, message, force, _mqtt_settings.retain); } bool mqttSend(const char* topic, const char* message) { return mqttSend(topic, message, false); } bool mqttSend(const char* topic, unsigned int index, const char* message, bool force, bool retain) { const size_t TopicLen { strlen(topic) }; String out; out.reserve(TopicLen + 5); out.concat(topic, TopicLen); out += '/'; out += index; return mqttSend(out.c_str(), message, force, retain); } bool mqttSend(const char* topic, unsigned int index, const char* message, bool force) { return mqttSend(topic, index, message, force, _mqtt_settings.retain); } bool mqttSend(const char* topic, unsigned int index, const char* message) { return mqttSend(topic, index, message, false); } // ----------------------------------------------------------------------------- constexpr size_t MqttJsonPayloadBufferSize { 1024ul }; void mqttFlush() { if (!_mqtt.connected()) { return; } if (_mqtt_json_payload.empty()) { return; } DynamicJsonBuffer jsonBuffer(MqttJsonPayloadBufferSize); JsonObject& root = jsonBuffer.createObject(); #if NTP_SUPPORT && MQTT_ENQUEUE_DATETIME if (ntpSynced()) { root[MQTT_TOPIC_DATETIME] = ntpDateTime(); } #endif #if MQTT_ENQUEUE_MAC root[MQTT_TOPIC_MAC] = WiFi.macAddress(); #endif #if MQTT_ENQUEUE_HOSTNAME root[MQTT_TOPIC_HOSTNAME] = systemHostname(); #endif #if MQTT_ENQUEUE_IP root[MQTT_TOPIC_IP] = wifiStaIp().toString(); #endif #if MQTT_ENQUEUE_MESSAGE_ID root[MQTT_TOPIC_MESSAGE_ID] = (Rtcmem->mqtt)++; #endif // ref. https://github.com/xoseperez/espurna/issues/2503 // pretend that the message is already a valid json value // when the string looks like a number // ([0-9] with an optional decimal separator [.]) for (auto& payload : _mqtt_json_payload) { const char* const topic { payload.topic().c_str() }; const char* const message { payload.message().c_str() }; if (isNumber(payload.message())) { root[topic] = RawJson(message); } else { root[topic] = message; } } String output; root.printTo(output); jsonBuffer.clear(); _mqtt_json_payload_count = 0; _mqtt_json_payload.clear(); mqttSendRaw(_mqtt_settings.topic_json.c_str(), output.c_str(), false); } void mqttEnqueue(espurna::StringView topic, espurna::StringView payload) { // Queue is not meant to send message "offline" // We must prevent the queue does not get full while offline if (_mqtt.connected()) { if (_mqtt_json_payload_count >= MQTT_QUEUE_MAX_SIZE) { mqttFlush(); } _mqtt_json_payload.remove_if( [topic](const MqttPayload& payload) { return topic == payload.topic(); }); _mqtt_json_payload.emplace_front( topic.toString(), payload.toString()); ++_mqtt_json_payload_count; } } // ----------------------------------------------------------------------------- // Only async client returns resulting PID, sync libraries return either success (1) or failure (0) uint16_t mqttSubscribeRaw(const char* topic, int qos) { uint16_t pid { 0u }; if (_mqtt.connected() && (strlen(topic) > 0)) { pid = _mqtt.subscribe(topic, qos); DEBUG_MSG_P(PSTR("[MQTT] Subscribing to %s (PID %d)\n"), topic, pid); } return pid; } uint16_t mqttSubscribeRaw(const char* topic) { return mqttSubscribeRaw(topic, _mqtt_settings.qos); } bool mqttSubscribe(const char* topic) { return mqttSubscribeRaw(mqttTopicSetter(topic).c_str(), _mqtt_settings.qos); } uint16_t mqttUnsubscribeRaw(const char* topic) { uint16_t pid { 0u }; if (_mqtt.connected() && (strlen(topic) > 0)) { pid = _mqtt.unsubscribe(topic); DEBUG_MSG_P(PSTR("[MQTT] Unsubscribing from %s (PID %d)\n"), topic, pid); } return pid; } bool mqttUnsubscribe(const char* topic) { return mqttUnsubscribeRaw(mqttTopicSetter(topic).c_str()); } // ----------------------------------------------------------------------------- void mqttEnabled(bool status) { _mqtt_enabled = status; } bool mqttEnabled() { return _mqtt_enabled; } bool mqttConnected() { return _mqtt.connected(); } void mqttDisconnect() { if (_mqtt.connected()) { DEBUG_MSG_P(PSTR("[MQTT] Disconnecting\n")); _mqtt.disconnect(); } } bool mqttForward() { return _mqtt_forward; } /** Register a persistent lifecycle callback @param standalone function pointer */ void mqttRegister(MqttCallback callback) { _mqtt_callbacks.push_front(callback); } #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT /** Register a temporary publish callback @param callable object */ void mqttOnPublish(uint16_t pid, MqttPidCallback callback) { _mqtt_publish_callbacks.push_front( MqttPidCallbackHandler{ .pid = pid, .callback = std::move(callback), }); } /** Register a temporary subscribe callback @param callable object */ void mqttOnSubscribe(uint16_t pid, MqttPidCallback callback) { _mqtt_subscribe_callbacks.push_front( MqttPidCallbackHandler{ .pid = pid, .callback = std::move(callback), }); } #endif // TODO: these strings are only updated after running the configuration routine and when MQTT is *enabled* const String& mqttPayloadOnline() { return _mqtt_payload_online; } const String& mqttPayloadOffline() { return _mqtt_payload_offline; } const char* mqttPayloadStatus(bool status) { return status ? _mqtt_payload_online.c_str() : _mqtt_payload_offline.c_str(); } void mqttSendStatus() { mqttSendRaw(_mqtt_settings.will.c_str(), _mqtt_payload_online.c_str(), true); } // ----------------------------------------------------------------------------- // Initialization // ----------------------------------------------------------------------------- namespace { void _mqttConnect() { // Do not connect if already connected or still trying to connect if (_mqtt.connected() || (_mqtt_state != AsyncClientState::Disconnected)) return; // Do not connect if disabled or no WiFi if (!_mqtt_enabled || (!wifiConnected())) return; // Check reconnect interval if (MqttTimeSource::now() - _mqtt_last_connection < _mqtt_reconnect_delay) return; // Increase the reconnect delay each attempt _mqtt_reconnect_delay += mqtt::build::ReconnectStep; _mqtt_reconnect_delay = std::clamp(_mqtt_reconnect_delay, mqtt::build::ReconnectDelayMin, mqtt::build::ReconnectDelayMax); DEBUG_MSG_P(PSTR("[MQTT] Connecting to broker at %s:%hu\n"), _mqtt_settings.server.c_str(), _mqtt_settings.port); _mqtt_state = AsyncClientState::Connecting; _mqtt_skip_messages = (_mqtt_skip_time.count() > 0); #if SECURE_CLIENT != SECURE_CLIENT_NONE const bool secure = mqtt::settings::secure(); #else const bool secure = false; #endif #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT _mqttSetupAsyncClient(secure); #elif (MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT) || (MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT) if (_mqttSetupSyncClient(secure) && _mqttConnectSyncClient(secure)) { _mqttOnConnect(); } else { DEBUG_MSG_P(PSTR("[MQTT] Connection failed\n")); _mqttOnDisconnect(); } #else #error "please check that MQTT_LIBRARY is valid" #endif } } // namespace void mqttLoop() { #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT _mqttConnect(); #else if (_mqtt.connected()) { _mqtt.loop(); } else { if (_mqtt_state != AsyncClientState::Disconnected) { _mqttOnDisconnect(); } _mqttConnect(); } #endif } void mqttHeartbeat(espurna::heartbeat::Callback callback) { _mqtt_heartbeat_callbacks.push_front(callback); } void mqttSetup() { _mqttBackwards(); _mqttInfo(); mqtt::settings::query::setup(); #if MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT // XXX: should not place this in config, addServerFingerprint does not check for duplicates #if SECURE_CLIENT != SECURE_CLIENT_NONE { if (_mqtt_sc_config.on_fingerprint) { const String fingerprint = _mqtt_sc_config.on_fingerprint(); uint8_t buffer[20] = {0}; if (sslFingerPrintArray(fingerprint.c_str(), buffer)) { _mqtt.addServerFingerprint(buffer); } } } #endif // SECURE_CLIENT != SECURE_CLIENT_NONE _mqtt.onMessage(_mqttOnMessageAsync); _mqtt.onConnect([](bool) { _mqttOnConnect(); }); _mqtt.onSubscribe([](uint16_t pid, int) { _mqttPidCallback(_mqtt_subscribe_callbacks, pid); }); _mqtt.onPublish([](uint16_t pid) { _mqttPidCallback(_mqtt_publish_callbacks, pid); }); _mqtt.onDisconnect([](AsyncMqttClientDisconnectReason reason) { switch (reason) { case AsyncMqttClientDisconnectReason::TCP_DISCONNECTED: DEBUG_MSG_P(PSTR("[MQTT] TCP Disconnected\n")); break; case AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED: DEBUG_MSG_P(PSTR("[MQTT] Identifier Rejected\n")); break; case AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE: DEBUG_MSG_P(PSTR("[MQTT] Server unavailable\n")); break; case AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS: DEBUG_MSG_P(PSTR("[MQTT] Malformed credentials\n")); break; case AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED: DEBUG_MSG_P(PSTR("[MQTT] Not authorized\n")); break; case AsyncMqttClientDisconnectReason::TLS_BAD_FINGERPRINT: #if ASYNC_TCP_SSL_ENABLED DEBUG_MSG_P(PSTR("[MQTT] Bad fingerprint\n")); #endif break; case AsyncMqttClientDisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION: // This is never used by the AsyncMqttClient source #if 0 DEBUG_MSG_P(PSTR("[MQTT] Unacceptable protocol version\n")); #endif break; case AsyncMqttClientDisconnectReason::ESP8266_NOT_ENOUGH_SPACE: DEBUG_MSG_P(PSTR("[MQTT] Connect packet too big\n")); break; } _mqttOnDisconnect(); }); #elif MQTT_LIBRARY == MQTT_LIBRARY_ARDUINOMQTT _mqtt.onMessageAdvanced([](MQTTClient* , char topic[], char payload[], int length) { _mqttOnMessage(topic, payload, length); }); #elif MQTT_LIBRARY == MQTT_LIBRARY_PUBSUBCLIENT _mqtt.setCallback([](char* topic, byte* payload, unsigned int length) { _mqttOnMessage(topic, (char *) payload, length); }); #endif // MQTT_LIBRARY == MQTT_LIBRARY_ASYNCMQTTCLIENT _mqttConfigure(); mqttRegister(_mqttCallback); #if WEB_SUPPORT wsRegister() .onVisible(_mqttWebSocketOnVisible) .onData(_mqttWebSocketOnData) .onConnected(_mqttWebSocketOnConnected) .onKeyCheck(_mqttWebSocketOnKeyCheck); mqttRegister([](unsigned int type, espurna::StringView, espurna::StringView) { if ((type == MQTT_CONNECT_EVENT) || (type == MQTT_DISCONNECT_EVENT)) { wsPost(_mqttWebSocketOnData); } }); #endif #if TERMINAL_SUPPORT _mqttCommandsSetup(); #endif // Main callbacks espurnaRegisterLoop(mqttLoop); espurnaRegisterReload(_mqttConfigure); } #endif // MQTT_SUPPORT